├── .node-version ├── src ├── scss │ ├── section │ │ ├── _index.scss │ │ ├── _footer.scss │ │ └── _header.scss │ ├── config │ │ ├── _styles.scss │ │ ├── _index.scss │ │ ├── _dark-mode.scss │ │ ├── _font.scss │ │ ├── _config.scss │ │ └── _dark-colors.scss │ ├── component │ │ ├── _group.scss │ │ ├── _accordion-list.scss │ │ ├── _skip-link.scss │ │ ├── _open-search.scss │ │ ├── _toc.scss │ │ ├── _index.scss │ │ ├── _theme-switcher.scss │ │ ├── _modal.scss │ │ ├── _changelog-item.scss │ │ ├── _sidebar.scss │ │ ├── _post-card.scss │ │ ├── _post-heading.scss │ │ ├── _breadcrumb-list.scss │ │ ├── _post-content.scss │ │ ├── _pagefind.scss │ │ ├── _sidebar-section.scss │ │ ├── _post-navigation.scss │ │ ├── _accordion-card.scss │ │ └── _prism.scss │ ├── layout │ │ ├── _index.scss │ │ ├── _front-page.scss │ │ ├── _card.scss │ │ ├── _list.scss │ │ ├── _container.scss │ │ ├── _post.scss │ │ └── _main.scss │ └── main.scss ├── posts │ ├── posts.json │ ├── customization │ │ ├── customization.md │ │ └── posts │ │ │ ├── posts.json │ │ │ ├── themes.md │ │ │ └── spruce-css.md │ └── getting-started │ │ ├── getting-started.md │ │ └── posts │ │ ├── posts.json │ │ ├── setup.md │ │ ├── content-management.md │ │ ├── introduction.md │ │ ├── features.md │ │ └── structure.md ├── img │ ├── cover │ │ ├── sprucecss.png │ │ └── sprucecss-light.png │ ├── icon │ │ ├── system-mode.svg │ │ ├── arrow-outward.svg │ │ ├── arrow-forward.svg │ │ ├── arrow-back.svg │ │ ├── menu.svg │ │ ├── search.svg │ │ ├── dark-mode.svg │ │ ├── twitter.svg │ │ ├── github.svg │ │ ├── discord.svg │ │ └── light-mode.svg │ ├── favicon.svg │ └── cone-docs-logo.svg ├── filters │ ├── w3-date-filter.js │ ├── date-filter.js │ └── parent-filter.js ├── font │ ├── manrope-v14-latin-600.woff2 │ ├── manrope-v14-latin-800.woff2 │ ├── manrope-v14-latin-regular.woff2 │ ├── open-sans-v35-latin-700.woff2 │ └── open-sans-v35-latin-regular.woff2 ├── shortcodes │ ├── markdown-render.js │ └── svg-icon.js ├── _data │ ├── global.js │ ├── site.json │ ├── helpers.js │ └── navigation.json ├── _includes │ ├── partials │ │ ├── search-modal.html │ │ ├── search-toggle.html │ │ ├── breadcrumbs.html │ │ ├── preload.html │ │ ├── theme-switcher.html │ │ ├── post-heading.html │ │ ├── post-navigation.html │ │ ├── footer.html │ │ ├── meta.html │ │ ├── sidebar.html │ │ └── header.html │ └── layouts │ │ ├── post.html │ │ ├── list.html │ │ ├── changelog.html │ │ ├── faq.html │ │ ├── base.html │ │ └── front-page.html ├── transforms │ └── html-min-transform.js ├── js │ ├── theme-detection.js │ ├── theme-change-assets.js │ ├── navigation.js │ ├── theme-switcher.js │ ├── modal.js │ └── accordion-card.js ├── changelog.md ├── index.md ├── faq.md └── css │ └── main.css ├── .github ├── sprucecss-cover.png ├── spruce-docs-preview-mockup-2.png ├── workflows │ └── test.yml ├── spruce-logo-dark.svg └── spruce-logo-light.svg ├── .editorconfig ├── .eslintrc.json ├── .stylelintrc.json ├── LICENSE ├── package.json ├── .eleventy.js ├── .gitignore ├── CODE_OF_CONDUCT.md └── README.md /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /src/scss/section/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'header'; 2 | @forward 'footer'; 3 | -------------------------------------------------------------------------------- /src/scss/config/_styles.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | @include generate-styles; 4 | -------------------------------------------------------------------------------- /src/posts/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/list.html", 3 | "permalink": "/{{ title | slug }}/index.html" 4 | } 5 | -------------------------------------------------------------------------------- /src/scss/component/_group.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .group { 4 | @include layout-stack; 5 | } 6 | -------------------------------------------------------------------------------- /.github/sprucecss-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/.github/sprucecss-cover.png -------------------------------------------------------------------------------- /src/img/cover/sprucecss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/img/cover/sprucecss.png -------------------------------------------------------------------------------- /src/scss/component/_accordion-list.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .accordion-list { 4 | @include layout-stack('s'); 5 | } 6 | -------------------------------------------------------------------------------- /src/scss/config/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'config'; 2 | @forward 'font'; 3 | @forward 'styles'; 4 | @forward 'dark-colors'; 5 | @forward 'dark-mode'; 6 | -------------------------------------------------------------------------------- /src/filters/w3-date-filter.js: -------------------------------------------------------------------------------- 1 | module.exports = (value) => { 2 | const dateObject = new Date(value); 3 | return dateObject.toISOString(); 4 | }; 5 | -------------------------------------------------------------------------------- /src/posts/customization/customization.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Customization" 3 | eleventyNavigation: 4 | key: Customization 5 | order: 2 6 | --- 7 | -------------------------------------------------------------------------------- /src/posts/customization/posts/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/post.html", 3 | "permalink": "/customization/{{ title | slug }}/index.html" 4 | } 5 | -------------------------------------------------------------------------------- /src/posts/getting-started/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | eleventyNavigation: 4 | key: Getting Started 5 | order: 1 6 | --- 7 | -------------------------------------------------------------------------------- /src/img/cover/sprucecss-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/img/cover/sprucecss-light.png -------------------------------------------------------------------------------- /src/posts/getting-started/posts/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/post.html", 3 | "permalink": "/getting-started/{{ title | slug }}/index.html" 4 | } 5 | -------------------------------------------------------------------------------- /src/font/manrope-v14-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/font/manrope-v14-latin-600.woff2 -------------------------------------------------------------------------------- /src/font/manrope-v14-latin-800.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/font/manrope-v14-latin-800.woff2 -------------------------------------------------------------------------------- /src/scss/layout/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'container'; 2 | @forward 'main'; 3 | @forward 'card'; 4 | @forward 'front-page'; 5 | @forward 'post'; 6 | @forward 'list'; 7 | -------------------------------------------------------------------------------- /.github/spruce-docs-preview-mockup-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/.github/spruce-docs-preview-mockup-2.png -------------------------------------------------------------------------------- /src/font/manrope-v14-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/font/manrope-v14-latin-regular.woff2 -------------------------------------------------------------------------------- /src/font/open-sans-v35-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/font/open-sans-v35-latin-700.woff2 -------------------------------------------------------------------------------- /src/font/open-sans-v35-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss-eleventy-documentation-template/HEAD/src/font/open-sans-v35-latin-regular.woff2 -------------------------------------------------------------------------------- /src/scss/layout/_front-page.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .l-front-page { 4 | display: grid; 5 | gap: spacer('xl'); 6 | grid-template-columns: minmax(0, 1fr); 7 | } 8 | -------------------------------------------------------------------------------- /src/filters/date-filter.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | module.exports = (value, locale) => { 4 | moment.locale(locale); 5 | const dateObject = moment(value); 6 | return dateObject.format('MMMM DD, YYYY'); 7 | }; 8 | -------------------------------------------------------------------------------- /src/shortcodes/markdown-render.js: -------------------------------------------------------------------------------- 1 | const markdownIt = require('markdown-it'); // eslint-disable-line 2 | 3 | module.exports = async (children) => { 4 | const md = new markdownIt(); // eslint-disable-line 5 | return md.render(children); 6 | }; 7 | -------------------------------------------------------------------------------- /src/_data/global.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | module.exports = { 4 | random() { 5 | return crypto.randomUUID(); 6 | }, 7 | year() { 8 | const date = new Date(); 9 | return date.getFullYear(); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/scss/component/_skip-link.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .skip-link { 4 | inset: -50vh auto auto spacer('m'); 5 | position: fixed; 6 | z-index: 100; 7 | 8 | &:focus { 9 | inset-block-start: spacer('m'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/filters/parent-filter.js: -------------------------------------------------------------------------------- 1 | module.exports = (collection, parent) => { 2 | if (!parent) return collection; 3 | const filtered = collection.filter((item) => item.data.eleventyNavigation?.parent === parent); 4 | return filtered.sort((a, b) => a.data.eleventyNavigation.order - b.data.eleventyNavigation.order); 5 | }; 6 | -------------------------------------------------------------------------------- /src/_includes/partials/search-modal.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/scss/layout/_card.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .l-card { 4 | --columns: 1; 5 | counter-reset: card; 6 | display: grid; 7 | gap: spacer('l'); 8 | grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); 9 | 10 | @include breakpoint('lg') { 11 | --columns: 2; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/layout/_list.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .l-list { 4 | --gtc: minmax(0, 1fr); 5 | 6 | display: grid; 7 | gap: spacer('l'); 8 | grid-template-columns: var(--gtc); 9 | 10 | &__inner { 11 | &--changelog, 12 | &--faq { 13 | @include layout-stack('l'); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/_includes/partials/search-toggle.html: -------------------------------------------------------------------------------- 1 | 2 | {% svgIcon './src/img/icon/search.svg', 'open-search__icon' %} 3 | Search 4 | 7 | 8 | -------------------------------------------------------------------------------- /src/posts/customization/posts/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Themes" 3 | eleventyNavigation: 4 | key: "Themes" 5 | parent: "Customization" 6 | order: 7 7 | --- 8 | 9 | [Spruce uses CSS](https://sprucecss.com/docs/customization/themes/) custom properties to handle the theming. It means that you can easily overwrite the colors if needed (like in a case of a dark theme mode). 10 | -------------------------------------------------------------------------------- /src/scss/component/_open-search.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .open-search { 4 | @include a11y-card-link('.open-search__btn', true); 5 | align-items: center; 6 | display: flex; 7 | gap: spacer('xs'); 8 | 9 | &__icon { 10 | --dimension: 1rem; 11 | block-size: var(--dimension); 12 | color: color('icon', 'search'); 13 | inline-size: var(--dimension); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/transforms/html-min-transform.js: -------------------------------------------------------------------------------- 1 | const htmlmin = require('html-minifier'); 2 | 3 | module.exports = (value, outputPath) => { 4 | if (outputPath && outputPath.indexOf('.html') > -1) { 5 | return htmlmin.minify(value, { 6 | useShortDoctype: true, 7 | removeComments: true, 8 | collapseWhitespace: true, 9 | minifyCSS: true, 10 | }); 11 | } 12 | 13 | return value; 14 | }; 15 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @forward 'config'; 2 | @forward 'layout'; 3 | @forward 'section'; 4 | @forward 'component'; 5 | 6 | @use 'sprucecss/scss/spruce' as *; 7 | 8 | :root { 9 | @include set-css-variable(( 10 | --section-gap: clamp(5rem, 7vw, 7rem), 11 | --container-gap: spacer-clamp('m', 'l'), 12 | --max-content-inline-size: 70rem, 13 | --box-shadow: 0 0.75rem 1.25rem hsl(0 0% 0% / 5%) 14 | )); 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | block_comment_start = /* 13 | block_comment = * 14 | block_comment_end = */ 15 | 16 | [*.{scss,css,js,json,yml,md}] 17 | indent_size = 2 18 | 19 | [acf-json/*.json] 20 | insert_final_newline = unset 21 | -------------------------------------------------------------------------------- /src/scss/layout/_container.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .container { 4 | --inline-size: #{config('container-inline-size', $layout)}; 5 | --gap: #{get-css-variable(--container-gap)}; 6 | 7 | @include layout-center( 8 | var(--gap), 9 | var(--inline-size) 10 | ); 11 | 12 | &--wide { 13 | --inline-size: 94rem; 14 | } 15 | 16 | &--narrow { 17 | --inline-size: 50rem; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "globals": { 14 | "google": true 15 | }, 16 | "ignorePatterns": [], 17 | "rules": { 18 | "no-param-reassign": 0, 19 | "no-shadow": "off", 20 | "no-new": "off" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/scss/config/_dark-mode.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | @include generate-color-variables( 4 | $dark-colors, 5 | ':root[data-theme-mode="dark"]' 6 | ); 7 | 8 | [data-theme-mode='dark'] { 9 | color-scheme: dark; 10 | 11 | select.form-control:not([multiple]):not([size]) { /* stylelint-disable-line */ 12 | @include field-icon( 13 | config('select', $form-icon, false), 14 | color('select-foreground', 'form', true, $dark-colors) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/_includes/partials/breadcrumbs.html: -------------------------------------------------------------------------------- 1 |
    2 |
  1. 3 | Home 4 |
  2. 5 | {%- for entry in breadcrumbs %} 6 |
  3. 7 | {% if entry.url == page.url %} 8 | {{ entry.title }} 9 | {% else %} 10 | {{ entry.title }} 11 | {% endif %} 12 |
  4. 13 | {%- endfor %} 14 |
15 | -------------------------------------------------------------------------------- /src/scss/component/_toc.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .toc { 4 | @include layout-stack('s'); 5 | 6 | &__title { 7 | font-size: font-size('h4'); 8 | } 9 | 10 | &__navigation { 11 | ol { 12 | @include clear-list; 13 | @include layout-stack('xs'); 14 | } 15 | 16 | a { 17 | color: color('text'); 18 | text-decoration: none; 19 | 20 | &:hover, 21 | &:focus { 22 | color: color('primary'); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/component/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'skip-link'; 2 | @forward 'breadcrumb-list'; 3 | @forward 'open-search'; 4 | @forward 'theme-switcher'; 5 | @forward 'post-heading'; 6 | @forward 'post-content'; 7 | @forward 'toc'; 8 | @forward 'sidebar'; 9 | @forward 'sidebar-section'; 10 | @forward 'post-card'; 11 | @forward 'prism'; 12 | @forward 'modal'; 13 | @forward 'pagefind'; 14 | @forward 'accordion-list'; 15 | @forward 'accordion-card'; 16 | @forward 'changelog-item'; 17 | @forward 'group'; 18 | @forward 'post-navigation'; 19 | -------------------------------------------------------------------------------- /src/shortcodes/svg-icon.js: -------------------------------------------------------------------------------- 1 | const { parse, stringify } = require('himalaya'); // eslint-disable-line 2 | const Image = require('@11ty/eleventy-img'); // eslint-disable-line 3 | 4 | module.exports = async (src, cls) => { 5 | const metadata = await Image(src, { 6 | formats: ['svg'], 7 | dryRun: true, 8 | }); 9 | const svg = parse(metadata.svg[0].buffer.toString()); 10 | 11 | svg[0].attributes.push({ 12 | key: 'class', 13 | value: cls || 'icon', 14 | }); 15 | 16 | return stringify(svg); 17 | }; 18 | -------------------------------------------------------------------------------- /src/_data/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "authorEmail": "hello@conedevelopment.com", 3 | "authorName": "Cone", 4 | "copyright": "Some rights reserved.", 5 | "name": "Spruce CSS Eleventy Documentation Template", 6 | "slogen": "An Eleventy theme for documentation sites", 7 | "social": { 8 | "twitter": "https://twitter.com/conedevelopment", 9 | "github": "https://github.com/conedevelopment/sprucecss-eleventy-documentation-template", 10 | "discord": "" 11 | }, 12 | "url": "https://eleventy-documentation.sprucecss.com" 13 | } 14 | -------------------------------------------------------------------------------- /src/img/icon/system-mode.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/_includes/partials/preload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scss/component/_theme-switcher.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .no-transition { 4 | * { 5 | transition: none !important; 6 | } 7 | } 8 | 9 | .theme-switcher { 10 | color: color('text'); 11 | display: inline-flex; 12 | position: relative; 13 | 14 | &[data-theme-mode='system'] &__system-mode { display: flex; } 15 | &[data-theme-mode='light'] &__light-mode { display: flex; } 16 | &[data-theme-mode='dark'] &__dark-mode { display: flex; } 17 | 18 | button { 19 | display: none; 20 | 21 | > * { 22 | pointer-events: none; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/scss/config/_font.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | @include font-face( 4 | 'Manrope', 5 | '../../font/manrope-v14-latin-regular.woff2' 6 | ); 7 | 8 | @include font-face( 9 | 'Manrope', 10 | '../../font/manrope-v14-latin-600.woff2', 11 | 600 12 | ); 13 | 14 | @include font-face( 15 | 'Manrope', 16 | '../../font/manrope-v14-latin-800.woff2', 17 | 700 18 | ); 19 | 20 | @include font-face( 21 | 'Open Sans', 22 | '../../font/open-sans-v35-latin-regular.woff2' 23 | ); 24 | 25 | @include font-face( 26 | 'Open Sans', 27 | '../../font/open-sans-v35-latin-700.woff2', 28 | 700 29 | ); 30 | -------------------------------------------------------------------------------- /src/scss/component/_modal.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .modal-backdrop { 4 | backdrop-filter: blur(0.35rem); 5 | background-color: hsl(0deg 0% 0% / 35%); 6 | display: none; 7 | inset: 0; 8 | padding: spacer('s'); 9 | position: fixed; 10 | 11 | &--open { 12 | display: block; 13 | } 14 | } 15 | 16 | .modal { 17 | background-color: color('background'); 18 | border-radius: config('border-radius-sm', $display); 19 | box-shadow: get-css-variable(--box-shadow); 20 | margin-block: 7rem; 21 | margin-inline: auto; 22 | max-inline-size: 40rem; 23 | padding: spacer-clamp('s', 'm'); 24 | } 25 | -------------------------------------------------------------------------------- /src/scss/layout/_post.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .l-post { 4 | --gtc: minmax(0, 1fr); 5 | 6 | display: grid; 7 | gap: get-css-variable(--container-gap); 8 | grid-template-columns: var(--gtc); 9 | 10 | @include breakpoint('lg') { 11 | --gtc: minmax(0, 1fr) minmax(0, 15rem); 12 | } 13 | 14 | &__toc { 15 | @include breakpoint('lg') { 16 | order: 2; 17 | } 18 | 19 | .toc { 20 | inset-block-start: spacer('l'); 21 | position: sticky; 22 | } 23 | } 24 | 25 | .post-heading { 26 | @include breakpoint('lg') { 27 | grid-column: 1 / 3; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/img/icon/arrow-outward.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/_data/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Returns back some attributes based on whether the 4 | * link is active or a parent of an active item 5 | * 6 | * @param {String} itemUrl The link in question 7 | * @param {String} pageUrl The page context 8 | * @returns {String} The attributes or empty 9 | */ 10 | getLinkActiveState(itemUrl, pageUrl) { 11 | let response = ''; 12 | 13 | if (itemUrl === pageUrl) { 14 | response = ' aria-current="page"'; 15 | } 16 | 17 | if (itemUrl.length > 1 && pageUrl.indexOf(itemUrl) === 0) { 18 | response += ' data-state="active"'; 19 | } 20 | 21 | return response; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/img/icon/arrow-forward.svg: -------------------------------------------------------------------------------- 1 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /src/js/theme-detection.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const systemMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 3 | const preferredTheme = localStorage.getItem('preferred-theme'); 4 | 5 | function setTheme(theme) { 6 | document.documentElement.setAttribute('data-theme-mode', theme === 'system' ? systemMode : theme); 7 | } 8 | 9 | window 10 | .matchMedia('(prefers-color-scheme: dark)') 11 | .addEventListener('change', (e) => { 12 | if (localStorage.getItem('preferred-theme') === 'system' || localStorage.getItem('preferred-theme') === null) { 13 | setTheme(e.matches ? 'dark' : 'light'); 14 | } 15 | }); 16 | 17 | setTheme(preferredTheme || systemMode); 18 | })(); 19 | -------------------------------------------------------------------------------- /src/img/icon/arrow-back.svg: -------------------------------------------------------------------------------- 1 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /src/img/icon/menu.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/img/icon/search.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-sass-guidelines", 3 | "plugins": ["stylelint-order"], 4 | "rules": { 5 | "selector-max-compound-selectors": 5, 6 | "max-nesting-depth": 4, 7 | "selector-no-vendor-prefix": [ 8 | true, 9 | { 10 | "ignoreSelectors": ["/-moz-.*/", "/-ms-.*/", "/-webkit-.*/"] 11 | } 12 | ], 13 | "selector-no-qualifying-type": [ 14 | true, 15 | { 16 | "ignore": ["attribute"] 17 | } 18 | ], 19 | "value-no-vendor-prefix": [ 20 | true, 21 | { 22 | "ignoreValues": ["box"] 23 | } 24 | ], 25 | "selector-class-pattern": null, 26 | "scss/percent-placeholder-pattern": null, 27 | "order/properties-alphabetical-order": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/img/icon/dark-mode.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/scss/component/_changelog-item.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .changelog-item { 4 | --gtc: minmax(0, 1fr); 5 | align-items: flex-start; 6 | display: grid; 7 | gap: spacer('m') spacer('l'); 8 | grid-template-columns: var(--gtc); 9 | 10 | @include breakpoint('md') { 11 | --gtc: minmax(0, 9rem) minmax(0, 1fr); 12 | } 13 | 14 | &__date { 15 | background-color: color('primary-background', 'btn'); 16 | border-radius: config('border-radius-sm', $display); 17 | color: color('primary-foreground', 'btn'); 18 | display: inline-flex; 19 | flex-shrink: 0; 20 | font-weight: 700; 21 | justify-self: start; 22 | padding: spacer('xxs') spacer('s'); 23 | } 24 | 25 | ul { 26 | p { 27 | margin-block: 0; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scss/component/_sidebar.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .sidebar { 4 | display: flex; 5 | flex-direction: column; 6 | gap: spacer('m'); 7 | 8 | @include breakpoint('sm') { 9 | padding-inline-end: get-css-variable(--container-gap); 10 | } 11 | 12 | &__footer { 13 | align-items: center; 14 | display: flex; 15 | justify-content: space-between; 16 | } 17 | 18 | &__body { 19 | @include scrollbar( 20 | $border-radius: 0.05rem 21 | ); 22 | display: flex; 23 | flex-direction: column; 24 | flex-grow: 1; 25 | gap: spacer('m'); 26 | max-block-size: calc(100vh - var(--dimension) - #{spacer('m')} * 3); 27 | overflow-y: auto; 28 | 29 | @include breakpoint('sm') { 30 | max-block-size: initial; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/scss/component/_post-card.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .post-card { 4 | $this: &; 5 | 6 | @include layout-stack('s'); 7 | @include a11y-card-link('.card__link', true); 8 | background-color: color('background', 'card'); 9 | border: 1px solid color('border'); 10 | border-radius: config('border-radius-sm', $display); 11 | padding: spacer('l') spacer-clamp('m', 'l'); 12 | 13 | &__serial-number { 14 | color: color('primary'); 15 | font-family: config('font-family-heading', $typography); 16 | font-size: responsive-font-size(4rem, 30, '4vw + 1rem'); 17 | font-weight: 800; 18 | line-height: 0.8; 19 | 20 | &::before { 21 | content: counter(card, decimal-leading-zero); 22 | counter-increment: card; 23 | } 24 | } 25 | 26 | &__title { 27 | font-size: font-size('h3'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/scss/component/_post-heading.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .post-heading { 4 | @include layout-stack('s'); 5 | 6 | &__headline { 7 | color: color('primary'); 8 | font-family: config('font-family-heading', $typography); 9 | font-weight: 700; 10 | letter-spacing: 0.1em; 11 | text-transform: uppercase; 12 | } 13 | 14 | &__title { 15 | font-size: responsive-font-size(3.5rem, 30, '4.5vw'); 16 | line-height: config('line-height-sm', $typography); 17 | margin-block-start: spacer('xs'); 18 | max-inline-size: 20ch; 19 | } 20 | 21 | &__description { 22 | font-size: config('font-size-lead', $typography); 23 | max-inline-size: 60ch; 24 | } 25 | 26 | &__actions { 27 | display: flex; 28 | flex-wrap: wrap; 29 | gap: spacer('s'); 30 | margin-block-start: spacer('m'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/js/theme-change-assets.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const htmlElement = document.querySelector('html'); 3 | const systemMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 4 | 5 | function changeAssets(theme) { 6 | if (!theme) return; 7 | const themeAssets = document.querySelectorAll('img[data-theme-mode]'); 8 | 9 | themeAssets.forEach((el) => { 10 | el.src = el.getAttribute(`data-${theme}-asset`); 11 | }); 12 | } 13 | 14 | changeAssets(htmlElement.getAttribute('data-theme-mode') === 'system' ? systemMode : htmlElement.getAttribute('data-theme-mode')); 15 | 16 | const observer = new MutationObserver(() => { 17 | changeAssets(htmlElement.getAttribute('data-theme-mode') === 'system' ? systemMode : htmlElement.getAttribute('data-theme-mode')); 18 | }); 19 | 20 | observer.observe(htmlElement, { attributes: true }); 21 | })(); 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - 18 | name: Checkout repository 19 | uses: actions/checkout@v3 20 | - 21 | name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 'lts/*' 25 | - 26 | name: Install dependencies 27 | run: npm ci 28 | - 29 | name: Run linter 30 | run: npm run sass:lint 31 | - 32 | name: Check EditorConfig configuration 33 | run: test -f .editorconfig 34 | - 35 | name: Check adherence to EditorConfig 36 | uses: greut/eclint-action@v0 37 | with: 38 | eclint_args: | 39 | -exclude=css/* 40 | -------------------------------------------------------------------------------- /src/js/navigation.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const button = document.querySelector('[data-action="sidebar-toggle"]'); 3 | const menu = document.querySelector('.l-main__sidebar'); 4 | const mq = window.matchMedia('(max-width: 64em)'); 5 | 6 | if (!menu || typeof button === 'undefined') return; 7 | 8 | function widthChange(query) { 9 | button.setAttribute('aria-expanded', !query.matches); 10 | } 11 | 12 | button.addEventListener('click', () => { 13 | if (button.getAttribute('aria-expanded') === 'true') { 14 | button.setAttribute('aria-expanded', 'false'); 15 | menu.classList.remove('l-main__sidebar--open'); 16 | } else { 17 | button.setAttribute('aria-expanded', 'true'); 18 | menu.classList.add('l-main__sidebar--open'); 19 | menu.querySelector('a').focus(); 20 | } 21 | }); 22 | 23 | mq.addListener(widthChange); 24 | widthChange(mq); 25 | })(); 26 | -------------------------------------------------------------------------------- /src/_includes/layouts/post.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% set breadcrumbs = collections.all | eleventyNavigationBreadcrumb(title, { includeSelf: true }) %} 6 | {% include "partials/post-heading.html" %} 7 | {% if content | toc %} 8 |
9 |
10 |

On this page

11 | 12 |
13 |
14 | {% endif %} 15 |
16 | {{ content | safe }} 17 |
18 |
19 | {% include "partials/post-navigation.html" %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /src/_includes/partials/theme-switcher.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 16 | 23 |
24 | -------------------------------------------------------------------------------- /src/_includes/layouts/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block content %} 3 |
4 | {% set breadcrumbs = collections.all | eleventyNavigationBreadcrumb(title, { includeSelf: true }) %} 5 | {% include "partials/post-heading.html" %} 6 |
7 | {% set posts = collections.all | parentFilter(eleventyNavigation.key) %} 8 | {% for post in posts %} 9 |
10 | 11 |

{{ post.data.title }}

12 |

{{ post.data.summary }}

13 | 14 | Read More 15 | {{ post.title }} 16 | 17 |
18 | {% endfor %} 19 | 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /src/_includes/partials/post-heading.html: -------------------------------------------------------------------------------- 1 |
2 | {% if headline %} 3 |

{{ headline }}

4 | {% endif %} 5 | {% if breadcrumbs %} 6 | {% include "partials/breadcrumbs.html" %} 7 | {% endif %} 8 | {% if title %} 9 |

10 | {{ title }} 11 |

12 | {% endif %} 13 | {% if summary and displaySummary %} 14 |

15 | {{ summary }} 16 |

17 | {% endif %} 18 | {% if btns %} 19 |
20 | {% for btn in btns %} 21 | {% set cls = 'btn--primary btn--primary-shadow' %} 22 | {% if btn.type === 'outline' %} 23 | {% set cls = 'btn--outline-primary' %} 24 | {% endif %} 25 | {{ btn.caption }} 26 | {% endfor %} 27 |
28 | {% endif %} 29 |
30 | -------------------------------------------------------------------------------- /src/js/theme-switcher.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const themeSwitcher = document.querySelector('#theme-switcher'); 3 | const preferredTheme = localStorage.getItem('preferred-theme') ?? 'system'; 4 | 5 | if (!themeSwitcher) { 6 | return; 7 | } 8 | 9 | themeSwitcher.addEventListener('click', (e) => { 10 | if (!e.target.matches('[data-action]')) { 11 | return; 12 | } 13 | 14 | const theme = e.target.getAttribute('data-action'); 15 | const systemMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 16 | document.documentElement.classList.add('no-transition'); 17 | 18 | localStorage.setItem('preferred-theme', theme); 19 | document.documentElement.setAttribute('data-theme-mode', theme === 'system' ? systemMode : theme); 20 | themeSwitcher.setAttribute('data-theme-mode', theme); 21 | themeSwitcher.querySelector(`.theme-switcher__${theme}-mode`).focus(); 22 | document.documentElement.classList.remove('no-transition'); 23 | }); 24 | 25 | themeSwitcher.setAttribute('data-theme-mode', preferredTheme); 26 | })(); 27 | -------------------------------------------------------------------------------- /src/scss/component/_breadcrumb-list.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .breadcrumb-list { 4 | @include clear-list; 5 | align-items: center; 6 | display: flex; 7 | max-inline-size: 100%; 8 | overflow-x: auto; 9 | white-space: nowrap; 10 | 11 | > li { 12 | align-items: center; 13 | display: inline-flex; 14 | margin-block: 0; 15 | 16 | + li::before { 17 | block-size: 0.4em; 18 | border-block-end: 2px solid color('separator', 'breadcrumb'); 19 | border-inline-end: 2px solid color('separator', 'breadcrumb'); 20 | content: ''; 21 | display: inline-flex; 22 | inline-size: 0.4em; 23 | margin-inline: 0.75em; 24 | transform: rotate(-45deg); 25 | 26 | @at-root { 27 | [dir='rtl'] & { 28 | transform: rotate(45deg); 29 | } 30 | } 31 | } 32 | } 33 | 34 | a { 35 | text-decoration: none; 36 | } 37 | 38 | [aria-current='page'] { 39 | @include text-ellipsis(1); 40 | display: inline-block; 41 | max-inline-size: 20ch; 42 | text-align: start; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/img/icon/twitter.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Cone Development 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 | -------------------------------------------------------------------------------- /src/_includes/layouts/changelog.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block content %} 3 |
4 | {% set breadcrumbs = [{ 5 | "title": title, 6 | "url": page.url 7 | }] %} 8 | {% include "partials/post-heading.html" %} 9 |
10 | {% for change in changelog %} 11 |
12 | 13 |
14 |

{{ change.title }}

15 |
    16 | {% for row in change.items %} 17 |
  • {% markdownRender row %}
  • 18 | {% endfor %} 19 |
20 |
21 |
22 | {% endfor %} 23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /src/posts/getting-started/posts/setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setup" 3 | summary: "We use eleventy --serve and compile Sass with sass-cli with npm scripts." 4 | eleventyNavigation: 5 | key: Setup 6 | parent: Getting Started 7 | order: 4 8 | --- 9 | 10 | ## Clone the repository 11 | 12 | Today more people and experts write about accessibility. For the better progression it is a good idea to read them. 13 | 14 | ## Install the dependencies 15 | 16 | In the `package.json` file, you will find all of the dependencies (and scripts) to install them using the following command: 17 | 18 | ```shell 19 | npm install 20 | ``` 21 | 22 | ## Run the development mode 23 | 24 | To run the development mode, use the `npm script`. This script will also watch for changes. 25 | 26 | ```shell 27 | npm start 28 | ``` 29 | 30 | ## Run the production mode 31 | 32 | Before you go live, you should use the production script to compress the Sass files. 33 | 34 | ```shell 35 | npm run prod 36 | ``` 37 | 38 | ## Additional Scripts 39 | 40 | You can find some more npm scripts in the [package.json](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/package.json) that can be helpful. 41 | -------------------------------------------------------------------------------- /src/scss/component/_post-content.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .post-content { 4 | @include layout-stack; 5 | 6 | @include breakpoint('md') { 7 | font-size: 1.0375rem; 8 | } 9 | 10 | :is(dd, dl, dl, h1, h2, h3:not(.accordion-card__title), h4, h5, h6, hr, ul, ol, p, blockquote) { 11 | max-inline-size: 40rem; 12 | } 13 | 14 | * + h2, 15 | * + h3 { 16 | margin-top: spacer('l'); 17 | } 18 | 19 | h2 + *, 20 | h3 + *, 21 | h4 + *, 22 | h5 + *, 23 | h6 + * { 24 | margin-top: spacer('s'); 25 | } 26 | 27 | h2[id] { 28 | align-items: flex-start; 29 | display: flex; 30 | justify-content: space-between; 31 | 32 | &:hover .anchor, 33 | &:focus-within .anchor { 34 | opacity: 1; 35 | } 36 | 37 | .anchor { 38 | font-weight: 600; 39 | opacity: 0; 40 | text-decoration: none; 41 | } 42 | } 43 | 44 | img, 45 | iframe { 46 | border-radius: config('border-radius-sm', $display); 47 | } 48 | 49 | iframe { 50 | aspect-ratio: 16 / 9; 51 | } 52 | 53 | a > code { 54 | color: color('link'); 55 | } 56 | 57 | strong { 58 | color: color('heading'); 59 | } 60 | 61 | picture { 62 | display: flex; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/_includes/layouts/faq.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block content %} 3 |
4 | {% set breadcrumbs = [{ 5 | "title": title, 6 | "url": page.url 7 | }] %} 8 | {% include "partials/post-heading.html" %} 9 |
10 | {% for faq in faqs %} 11 |
12 | {% if faq.title %} 13 |

{{ faq.title }}

14 | {% endif %} 15 |
16 | {% for post in faq.items %} 17 |
18 |

{{ post.title }}

19 |
20 | {% markdownRender post.description %} 21 |
22 |
23 | {% endfor %} 24 |
25 |
26 | {% endfor %} 27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /src/scss/layout/_main.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .l-main { 4 | --gtc: minmax(0, 1fr); 5 | 6 | display: grid; 7 | gap: spacer('l'); 8 | grid-template-columns: var(--gtc); 9 | margin-block: spacer-clamp('m', 'l') spacer-clamp('l', 'xl'); 10 | 11 | @include breakpoint('sm') { 12 | --gtc: minmax(0, 16rem) minmax(0, 1fr); 13 | gap: calc(#{get-css-variable(--container-gap)} * 1.5); 14 | } 15 | 16 | @include breakpoint('md') { 17 | --gtc: minmax(0, 18.5rem) minmax(0, 1fr); 18 | } 19 | 20 | @include breakpoint('lg') { 21 | --gtc: minmax(0, 20rem) minmax(0, 1fr); 22 | } 23 | 24 | &__sidebar { 25 | display: none !important; 26 | 27 | @include breakpoint('sm') { 28 | border-inline-end: 1px solid color('border'); 29 | display: flex !important; 30 | inset-block-start: spacer('l'); 31 | max-block-size: calc(100vh - #{spacer('l')} * 2); 32 | position: sticky; 33 | } 34 | 35 | &--open { 36 | display: flex !important; 37 | } 38 | } 39 | 40 | &__body { 41 | display: flex; 42 | flex-direction: column; 43 | inline-size: 100%; 44 | margin-inline: auto; 45 | max-inline-size: get-css-variable(--max-content-inline-size); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/_includes/partials/post-navigation.html: -------------------------------------------------------------------------------- 1 | {% set previousPost = collections.posts | getPreviousCollectionItem %} 2 | {% set nextPost = collections.posts | getNextCollectionItem %} 3 | 4 | 28 | -------------------------------------------------------------------------------- /src/scss/component/_pagefind.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | @include generate-form-control('.pagefind-ui__search-input', false, false, false); 4 | 5 | .pagefind-ui { 6 | position: relative; 7 | 8 | &__search-input { 9 | padding: config('padding', $form-control, false) !important; 10 | } 11 | 12 | &__form { 13 | @include layout-stack('s'); 14 | } 15 | 16 | &__search-clear { 17 | background: none; 18 | border: 0; 19 | font-size: config('font-size-sm', $typography); 20 | inset-block-start: 0.8em; 21 | inset-inline-end: 0.5em; 22 | margin-block-start: 0; 23 | position: absolute; 24 | text-transform: uppercase; 25 | } 26 | 27 | &__drawer { 28 | @include scrollbar; 29 | max-block-size: 20rem; 30 | overflow-y: auto; 31 | } 32 | 33 | &__results { 34 | @include clear-list; 35 | @include layout-stack('s'); 36 | padding-inline-end: spacer('m'); 37 | 38 | &:empty { 39 | display: none; 40 | } 41 | } 42 | 43 | &__results-area { 44 | @include layout-stack('xs'); 45 | } 46 | 47 | &__result-inner { 48 | @include layout-stack('xxs'); 49 | } 50 | 51 | &__result-title { 52 | font-weight: 700; 53 | } 54 | 55 | &__hidden { 56 | display: none; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/posts/getting-started/posts/content-management.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Content Management" 3 | summary: "Today more people and experts write about accessibility. For the better progression it is a good idea to read them." 4 | eleventyNavigation: 5 | key: Content Management 6 | parent: Getting Started 7 | order: 5 8 | --- 9 | 10 | Adding content to the template is easy as almost everything is in Eleventy. 11 | 12 | ## The Basic Structure 13 | 14 | Our base folder for the documentation pages is the `posts` folder. You must follow the folder structure, which means the `category` here. If you create a folder, you must make a list page with the same name as the folder. You must also create another `posts` folder under the `category` folder where your posts go. You must create the `posts.json` file that will parameter your `layout` and `permalink` values. 15 | 16 | ## Eleventy Navigation 17 | 18 | The theme utilizes the [Eleventy Navigation plugin](https://www.11ty.dev/docs/plugins/navigation/), so you must explicitly set up the hierarchy. This is needed for the automatic sidebar navigation, the navigation order, and breadcrumb generation. 19 | 20 | ## Other Pages 21 | 22 | To create simple pages, make a file directly under the `src` folder and configure it with the available front matter. 23 | -------------------------------------------------------------------------------- /src/posts/getting-started/posts/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction" 3 | summary: "Welcome to the official documentation of Spruce Docs Elventy theme. A small template that you can use to document any of your projects." 4 | eleventyNavigation: 5 | key: Introduction 6 | parent: Getting Started 7 | order: 1 8 | --- 9 | 10 | Welcome to the official documentation of **Spruce Docs** Elventy theme. A small template that you can use to document any of your projects. 11 | 12 | ## About the Template 13 | 14 | A documentation template is always helpful. There are a lot of solutions to make one; we wanted to create our self-hosted version based on our favorite static site generator. 15 | 16 | By structure, it is simple, with two levels and additional custom templates like [FAQ](/faq/) and [Changelog](/changelog/). 17 | 18 | ## Spruce CSS 19 | 20 | The template is built on [Spruce CSS](https://sprucecss.com/), a small and customizable CSS framework. The main benefit of this is that you can use the Spruce UI components with dark mode and RTL support. 21 | 22 | A minimalistic, low-level CSS framework 29 | -------------------------------------------------------------------------------- /src/js/modal.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | let activeElement = null; 3 | const siteWrapper = document.querySelector('.site-wrapper'); 4 | const button = document.querySelector('[data-action="open-search"]'); 5 | const input = document.querySelector('.pagefind-ui__search-input'); 6 | const modal = document.querySelector('.modal-backdrop'); 7 | 8 | if (!button || !modal) return; 9 | 10 | function openModal() { 11 | activeElement = document.activeElement; 12 | siteWrapper.setAttribute('inert', ''); 13 | modal.classList.add('modal-backdrop--open'); 14 | input.focus(); 15 | } 16 | 17 | function closeModal() { 18 | siteWrapper.removeAttribute('inert'); 19 | modal.classList.remove('modal-backdrop--open'); 20 | activeElement.focus(); 21 | } 22 | 23 | function handleKeyDown(e) { 24 | if (e.code === 'Escape') { 25 | closeModal(); 26 | } 27 | 28 | if (e.ctrlKey && e.code === 'KeyK') { 29 | e.preventDefault(); 30 | openModal(); 31 | } 32 | } 33 | 34 | modal.addEventListener('click', (e) => { 35 | if (e.target === e.currentTarget) { 36 | closeModal(); 37 | } 38 | }); 39 | 40 | button.addEventListener('click', () => { 41 | openModal(); 42 | }); 43 | 44 | window.addEventListener('keydown', handleKeyDown); 45 | })(); 46 | -------------------------------------------------------------------------------- /src/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Changelog" 3 | layout: "layouts/changelog.html" 4 | changelog: 5 | - date: "2023-05-24" 6 | title: "v1.1.0" 7 | items: 8 | - "**This is a test changelog record for demonstration.**" 9 | - "**Improvement:** add `$btn-font-family` to control the button's font family." 10 | - "**Improvement:** add `$heading-font-weight` to control the heading's font-weight." 11 | - "**Improvement:** rename some keys in the `$colors` map (`mark-color` : `mark-foreground`, `code-color` : `code-foreground`)." 12 | - "**Improvement:** reorganize the recurrent colors into variables." 13 | - "**Fix:** modify `btn-variant()` mixin: add hover foreground color." 14 | - "Global switch to `color()` function's fallback value under the [`$settings`](https://sprucecss.com/docs/sass/variables#settings) map." 15 | - "Modify the [`scrollbar()`](https://sprucecss.com/docs/sass/mixins#scrollbar) mixin to accept hover thumb background-color value." 16 | - "Make `$breakpoints` overwriteable by key." 17 | - "Modify `font-size()` and `responsive-font-size()` function to accept optimal size value. The optimal value will fallback to a global settnigs under the `$settings` map." 18 | - date: "2023-05-23" 19 | title: "v1.0.0" 20 | items: 21 | - "Initial release" 22 | --- 23 | -------------------------------------------------------------------------------- /src/scss/section/_footer.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .footer { 4 | --logo-block-size: 4rem; 5 | --logo-inline-size: 4rem; 6 | 7 | border-block-start: 1px solid color('border'); 8 | margin-block-start: spacer-clamp('l', 'xl'); 9 | padding-block-start: spacer-clamp('l', 'xl'); 10 | 11 | &__inner { 12 | @include layout-grid('l', 12rem); 13 | } 14 | 15 | &__logo { 16 | block-size: var(--logo-block-size); 17 | display: flex; 18 | inline-size: var(--logo-inline-size); 19 | } 20 | 21 | &__title { 22 | font-size: font-size('h4'); 23 | } 24 | 25 | &__navigation { 26 | @include clear-list; 27 | @include layout-stack('xs'); 28 | 29 | a { 30 | align-items: center; 31 | color: color('text'); 32 | display: inline-flex; 33 | gap: spacer('xs'); 34 | text-decoration: none; 35 | 36 | &:hover, 37 | &:focus, 38 | &:active, 39 | &[aria-current='page'] { 40 | color: color('primary'); 41 | } 42 | 43 | &[aria-current='page'] { 44 | font-weight: 700; 45 | } 46 | 47 | svg { 48 | --dimension: 0.65em; 49 | block-size: var(--dimension); 50 | color: color('arrow', 'navigation'); 51 | inline-size: var(--dimension); 52 | } 53 | } 54 | } 55 | 56 | &__column { 57 | @include layout-stack('m'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/js/accordion-card.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | document.querySelectorAll('.accordion-card').forEach((accordion) => { 3 | const heading = accordion.querySelector('.accordion-card__title'); 4 | 5 | accordion.classList.add('accordion-card--js'); 6 | 7 | heading.nextElementSibling.hidden = true; 8 | heading.innerHTML = ` 9 | 16 | `; 17 | 18 | const btn = heading.querySelector('button'); 19 | 20 | btn.addEventListener('click', () => { 21 | const expanded = btn.getAttribute('aria-expanded') === 'true'; 22 | 23 | btn.setAttribute('aria-expanded', !expanded); 24 | heading.nextElementSibling.hidden = expanded; 25 | }); 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /src/img/icon/github.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/_includes/partials/footer.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /src/posts/getting-started/posts/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Features" 3 | summary: "We tried to make a simple but well-structured theme. Managing the content is straightforward but still comes with some helpful features." 4 | eleventyNavigation: 5 | key: Features 6 | parent: Getting Started 7 | order: 2 8 | --- 9 | 10 | We tried to make a simple but well-structured theme. Managing the content is straightforward but still comes with some helpful features. 11 | 12 | ## Eleventy 13 | 14 | - Breadcrumb navigation built on [11ty Navigation Plugin](https://www.11ty.dev/docs/plugins/navigation/). 15 | - HTML minification in production mode. 16 | - Anchor headings. 17 | - Table of Content. 18 | - FAQ template. 19 | - Changelog template. 20 | - Static search integration with [pagefind](https://pagefind.app/). 21 | - Code highlighting. 22 | - [svgIcon](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/src/shortcodes/svg-icon.js) shortcode: render any SVG icon inline and add optional classes. 23 | - [markdownRenderer](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/src/shortcodes/markdown-render.js): render any string (markdown) into HTML. 24 | 25 | ## CSS Customization 26 | 27 | The template utilizes Spruce CSS and some of its components. You can learn more about the customization on the [official documentation](https://sprucecss.com/). 28 | 29 | - RTL support. 30 | - Dark mode. 31 | -------------------------------------------------------------------------------- /src/posts/getting-started/posts/structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Structure" 3 | summary: "The structure is a generic Eleventy theme with the standard folder and file names." 4 | eleventyNavigation: 5 | key: Structure 6 | parent: Getting Started 7 | order: 3 8 | --- 9 | 10 | The structure is a generic Eleventy theme with the standard folder and file names. 11 | 12 | ## Tree View 13 | 14 | ```html 15 | spruecss-eleventy-documentation-template/ 16 | ├─ node_modules/ 17 | ├─ dist/ 18 | ├─ src/ 19 | │ ├─ _data/ 20 | │ ├─ _includes/ 21 | │ ├─ css/ 22 | │ ├─ filters/ 23 | │ ├─ font/ 24 | │ ├─ img/ 25 | │ ├─ js/ 26 | │ ├─ posts/ 27 | │ ├─ scss/ 28 | │ ├─ shortcodes/ 29 | │ ├─ transforms/ 30 | │ ├─ changelog.md 31 | │ ├─ faq.md 32 | │ ├─ index.md 33 | ├─ .eleventy.js 34 | ├─ package.json 35 | ├─ README.md 36 | ├─ ... 37 | 38 | ``` 39 | 40 | ## _data 41 | 42 | Some global data, like the name of your site and helpers like the active navigation element or current year. 43 | 44 | ## __includes 45 | 46 | All of the layout and partial templates. 47 | 48 | ## css 49 | 50 | The compiled CSS. 51 | 52 | ## filters 53 | 54 | The additional filters that you can use. 55 | 56 | ## font 57 | 58 | The custom fonts. 59 | 60 | ## img 61 | 62 | The static image files. 63 | 64 | ## posts 65 | 66 | The markdown contents. 67 | 68 | ## scss 69 | 70 | The Sass files. 71 | 72 | ## shortcodes 73 | 74 | The available shortcodes. 75 | 76 | ## transforms 77 | 78 | The transformations. 79 | -------------------------------------------------------------------------------- /src/scss/component/_sidebar-section.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .sidebar-section { 4 | &--navigation { 5 | @include breakpoint('sm') { 6 | display: none; 7 | } 8 | } 9 | 10 | &__title { 11 | color: color('heading'); 12 | font-size: config('font-size-base', $typography); 13 | margin-block: 0; 14 | } 15 | 16 | &__navigation { 17 | border-inline-start: 1px solid color('border'); 18 | font-size: 1rem; 19 | margin-block-start: 1rem; 20 | padding-inline-start: 1rem; 21 | 22 | ul { 23 | @include clear-list; 24 | } 25 | 26 | a { 27 | align-items: center; 28 | color: color('text'); 29 | display: inline-flex; 30 | gap: spacer('xs'); 31 | text-decoration: none; 32 | 33 | &[aria-current='page'] { 34 | color: color('heading'); 35 | font-weight: 700; 36 | position: relative; 37 | 38 | &::before { 39 | background-color: color('primary'); 40 | border-end-end-radius: config('border-radius-sm', $display); 41 | border-start-end-radius: config('border-radius-sm', $display); 42 | content: ''; 43 | inline-size: 0.3rem; 44 | inset-block: 0; 45 | inset-inline-start: -1rem; 46 | position: absolute; 47 | } 48 | } 49 | 50 | svg { 51 | --dimension: 0.65em; 52 | block-size: var(--dimension); 53 | color: color('arrow', 'navigation'); 54 | inline-size: var(--dimension); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/_includes/layouts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% include "partials/preload.html" %} 6 | 7 | 8 | 9 | 10 | {% include "partials/meta.html" %} 11 | 12 | 13 | 14 |
15 | 16 |
17 | {% include "partials/header.html" %} 18 |
19 | {% include "partials/sidebar.html" %} 20 |
21 |
{% block content %}{% endblock %}
22 | {% include "partials/footer.html" %} 23 |
24 |
25 |
26 |
27 | {% include "partials/search-modal.html" %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/posts/customization/posts/spruce-css.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Spruce CSS" 3 | eleventyNavigation: 4 | key: "Spruce CSS" 5 | parent: "Customization" 6 | order: 6 7 | --- 8 | 9 | **Spruce CSS is an open-source, lightweight and modernish CSS design system, framework built on Sass. Give your project a solid foundation.** 10 | 11 | ## What is Spruce CSS? 12 | 13 | - It is a Sass-based, small framework that operates with just a few utility classes. 14 | - It takes advantage of the Sass members: variables, mixins, and functions. 15 | - It embraces Sass modules, so it uses @use and namespacing for import. 16 | - Spruce is a good choice if you prefer writing CSS instead of HTML. It uses just a few classic utility classes. 17 | - It is a relatively small (~7kb gzipped) framework with a smaller learning curve. The codebase is small but can add more to any project with the available mixins and functions. 18 | - It is that bunch of code you keep manually carrying from project to project. 19 | - It is themeable. You can create different themes using CSS custom properties like a dark one. 20 | - The generated CSS code is separated from the framework. You can use only the tools (variables, mixins, functions) in your project [without the generated styles](https://sprucecss.com/docs/elements/generators). 21 | - Include just a few components. For UI, we have a separate project named [Spruce UI](/ui/getting-started/introduction), where you can find drop-in components. 22 | - [It comes with dark-mode](https://sprucecss.com/docs/customization/themes) (or any theme mode) support. It uses CSS custom properties, so it isn’t that hard to create a new color theme. 23 | - It doesn’t come with a classical grid system. 24 | -------------------------------------------------------------------------------- /src/scss/component/_post-navigation.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .post-navigation { 4 | align-items: center; 5 | border-block-start: 1px solid color('border'); 6 | display: flex; 7 | flex-wrap: wrap; 8 | gap: spacer('m'); 9 | justify-content: space-between; 10 | margin-block-start: spacer('xl'); 11 | padding-block: spacer('m'); 12 | } 13 | 14 | .post-navigation-item { 15 | $this: &; 16 | 17 | align-items: center; 18 | display: flex; 19 | gap: spacer('s'); 20 | text-decoration: none; 21 | 22 | &:hover { 23 | #{$this}__icon { 24 | background-color: color('icon-background-hover', 'navigation'); 25 | color: color('icon-foreground-hover', 'navigation'); 26 | } 27 | } 28 | 29 | &--next { 30 | margin-inline-start: auto; 31 | text-align: end; 32 | } 33 | 34 | &__icon { 35 | @include transition; 36 | align-items: center; 37 | background-color: color('icon-background', 'navigation'); 38 | block-size: 3rem; 39 | border-radius: config('border-radius', $btn, false); 40 | color: color('icon-foreground', 'navigation'); 41 | display: flex; 42 | flex-shrink: 0; 43 | inline-size: 3rem; 44 | justify-content: center; 45 | 46 | svg { 47 | --dimension: 1rem; 48 | 49 | block-size: var(--dimension); 50 | inline-size: var(--dimension); 51 | 52 | @at-root { 53 | [dir='rtl'] & { 54 | transform: rotate(180deg); 55 | } 56 | } 57 | } 58 | } 59 | 60 | &__caption { 61 | color: color('text'); 62 | line-height: config('line-height-md', $typography); 63 | } 64 | 65 | &__title { 66 | color: color('primary'); 67 | display: flex; 68 | font-weight: 700; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/_includes/layouts/front-page.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block content %} 3 |
4 | {% include "partials/post-heading.html" %} 5 | {% if overview %} 6 |
7 |

Overview

8 |
9 | {% for post in overview %} 10 |
11 | 12 |

{{ post.title }}

13 |

{{ post.description }}

14 | 15 | Read More 16 | {{ post.title }} 17 | 18 |
19 | {% endfor %} 20 |
21 |
22 | {% endif %} 23 | {% if faqs %} 24 |
25 |

Frequently Asked Questions

26 |
27 | {% for faq in faqs %} 28 |
29 |

{{ faq.title }}

30 |
31 | {% markdownRender faq.description %} 32 |
33 |
34 | {% endfor %} 35 |
36 |
37 | {% endif %} 38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /src/scss/component/_accordion-card.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .accordion-card { 4 | $this: &; 5 | 6 | background-color: color('background'); 7 | border: 1px solid color('border'); 8 | border-radius: config('border-radius-sm', $display); 9 | max-inline-size: 75ch; 10 | 11 | &--js { 12 | #{$this}__title { 13 | padding: 0; 14 | } 15 | } 16 | 17 | &__title { 18 | font-family: config('font-family-base', $typography); 19 | font-size: font-size('h4'); 20 | margin-block: 0; 21 | padding: spacer('m'); 22 | } 23 | 24 | &__toggle { 25 | @include clear-btn; 26 | align-items: center; 27 | display: flex; 28 | gap: spacer('m'); 29 | inline-size: 100%; 30 | justify-content: space-between; 31 | padding: spacer('m'); 32 | text-align: start; 33 | 34 | &:focus-visible { 35 | svg { 36 | @include focus-ring( 37 | $type: config('focus-ring-type', $btn, false), 38 | $ring-color: color('primary'), 39 | $ring-size: config('focus-ring-size', $btn, false), 40 | $ring-offset: config('focus-ring-offset', $btn, false) 41 | ); 42 | } 43 | } 44 | 45 | svg { 46 | --dimension: 1.75rem; 47 | 48 | background-color: color('primary-background', 'btn'); 49 | block-size: var(--dimension); 50 | border-radius: config('border-radius-sm', $display); 51 | color: color('primary-foreground', 'btn'); 52 | flex-shrink: 0; 53 | inline-size: var(--dimension); 54 | } 55 | 56 | &[aria-expanded='true'] .vertical-line { 57 | display: none; 58 | } 59 | } 60 | 61 | &__content { 62 | @include layout-stack('xs'); 63 | padding-block-end: spacer('m'); 64 | padding-inline: spacer('m'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/_includes/partials/meta.html: -------------------------------------------------------------------------------- 1 | {% if page.url.length > 1 %} 2 | {% set pageTitle = title + ' - ' + site.name %} 3 | {% else %} 4 | {% set pageTitle = site.name %} 5 | {% endif %} 6 | 7 | {% if site.name === title %} 8 | {% set pageTitle = title %} 9 | {% endif %} 10 | 11 | {% set siteTitle = site.name %} 12 | {% set currentUrl = site.url + page.url %} 13 | 14 | {% if not socialImage %} 15 | {% set socialImage = site.url + '/img/social-share.png' %} 16 | {% else %} 17 | {% set socialImage = site.url + socialImage %} 18 | {% endif %} 19 | 20 | {% if metaTitle %} 21 | {% set pageTitle = metaTitle %} 22 | {% endif %} 23 | 24 | {% if not metaDesc %} 25 | {% set metaDesc = summary %} 26 | {% endif %} 27 | 28 | {{ pageTitle }} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% if socialImage %} 39 | 40 | 41 | 42 | 43 | 44 | {% endif %} 45 | 46 | {% if metaDesc %} 47 | 48 | 49 | 50 | {% endif %} 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sprucecss-eleventy-documentation-template", 3 | "version": "1.2.0", 4 | "author": "Cone (https://conedevelopment.com)", 5 | "main": ".eleventy.js", 6 | "scripts": { 7 | "delete:dist": "del-cli --force dist", 8 | "eleventy:dev": "npx eleventy --serve", 9 | "eleventy:prod": "npx eleventy", 10 | "js:lint": "npx eslint \"src/**/*.js\"", 11 | "js:lint:fix": "npx eslint \"src/**/*.js**\" --fix", 12 | "prod": "npm-run-all delete:dist eleventy:prod sass:prod", 13 | "sass:dev": "sass --load-path=node_modules --watch --no-source-map --update --style=expanded src/scss:src/css", 14 | "sass:prod": "sass --load-path=node_modules --no-source-map --style=compressed src/scss:src/css", 15 | "sass:lint": "stylelint \"src/scss/**/*.scss\"", 16 | "sass:lint:fix": "stylelint \"src/scss/**/*.scss\" --fix", 17 | "start": "npm-run-all --parallel eleventy:dev sass:dev" 18 | }, 19 | "dependencies": { 20 | "@11ty/eleventy-img": "^3.1.0", 21 | "@11ty/eleventy": "^2.0.1", 22 | "@11ty/eleventy-navigation": "^0.3.5", 23 | "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", 24 | "del-cli": "^5.0.1", 25 | "dotenv": "^16.3.1", 26 | "eleventy-plugin-toc": "^1.1.5", 27 | "eslint": "^8.48.0", 28 | "eslint-config-airbnb-base": "^15.0.0", 29 | "eslint-plugin-import": "^2.28.1", 30 | "himalaya": "^1.1.0", 31 | "html-minifier": "^4.0.0", 32 | "moment": "^2.29.4", 33 | "markdown-it-anchor": "^8.6.7", 34 | "npm-run-all": "^4.1.5", 35 | "pagefind": "^0.12.0", 36 | "sass": "^1.66.1", 37 | "sprucecss": "^2.3.0", 38 | "stylelint": "^15.10.3", 39 | "stylelint-config-sass-guidelines": "^10.0.0", 40 | "stylelint-order": "^6.0.3" 41 | }, 42 | "engines": { 43 | "node": "^18.12", 44 | "npm": "^9.2", 45 | "yarn": "please-use-npm" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/img/icon/discord.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/img/icon/light-mode.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Document your next project a little bit better." 3 | headline: "Eleventy / Spruce CSS" 4 | btns: 5 | - 6 | caption: "Introduction" 7 | url: "/getting-started/introduction/" 8 | type: "regular" 9 | - 10 | caption: "Changelog" 11 | url: "/changelog/" 12 | type: "outline" 13 | summary: "Do you work on a project that requires a documentation? This theme is for you. It's a simple, clean and responsive theme for Eleventy." 14 | displaySummary: true 15 | layout: "layouts/front-page.html" 16 | overview: 17 | - 18 | title: "Getting Started" 19 | url: "/getting-started/" 20 | description: "Start here and get to know this minimalistic Eleventy theme." 21 | - 22 | title: "Customization" 23 | url: "/customization/" 24 | description: "Built on the top of Spruce CSS, you can easily customize its look." 25 | faqs: 26 | - 27 | title: "Why Make Another CSS Framework?" 28 | description: "As you may know, there are many CSS frameworks (hundreds of them, and a lot of them are not maintained today). Everybody can choose one that suits their work style or project requirements. So why make another one? It is certainly not because we can do it better but because we want to do it our way. We want to be in control and make decisions." 29 | - 30 | title: "It Is Opinionated" 31 | description: "Each system is opinionated but on a different level; this is valid for Spruce too. We don’t want to vote for (strictly) any particular solution (because there is always more than one), but we will show you what we think is the best for us (and maybe for you too). We don’t believe there is a good or bad solution, but we can learn from any of them." 32 | - 33 | title: "We Left the Grid Out" 34 | description: "One controversial decision we made with Spruce is to leave a classical grid system out. Because of the late CSS layout model developments like Flexbox and Grid, we think it can be eliminated; this doesn’t mean that we won’t show you how to make layouts with ease, but we try to make it the modern way." 35 | - 36 | title: "Coding Style Guide and Practices" 37 | description: "Where it is possible, we use elements and/or attributes to style elements, but it is still a class-based framework." 38 | --- 39 | -------------------------------------------------------------------------------- /src/_includes/partials/sidebar.html: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /src/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Frequently Asked Questions" 3 | summary: "Have some questions? You may find it here." 4 | displaySummary: true 5 | layout: "layouts/faq.html" 6 | faqs: 7 | - 8 | title: "Spruce CSS" 9 | items: 10 | - 11 | title: "Why Make Another CSS Framework?" 12 | description: "As you may know, there are many CSS frameworks (hundreds of them, and a lot of them are not maintained today). Everybody can choose one that suits their work style or project requirements. So why make another one? It is certainly not because we can do it better but because we want to do it our way. We want to be in control and make decisions." 13 | - 14 | title: "It Is Opinionated" 15 | description: "Each system is opinionated but on a different level; this is valid for Spruce too. We don’t want to vote for (strictly) any particular solution (because there is always more than one), but we will show you what we think is the best for us (and maybe for you too). We don’t believe there is a good or bad solution, but we can learn from any of them." 16 | - 17 | title: "We Left the Grid Out" 18 | description: "One controversial decision we made with Spruce is to leave a classical grid system out. Because of the late CSS layout model developments like Flexbox and Grid, we think it can be eliminated; this doesn’t mean that we won’t show you how to make layouts with ease, but we try to make it the modern way." 19 | - 20 | title: "Coding Style Guide and Practices" 21 | description: "Where it is possible, we use elements and/or attributes to style elements, but it is still a class-based framework." 22 | - 23 | title: "Demo FAQ" 24 | items: 25 | - 26 | title: "Lorem ipsum dolor sit amet" 27 | description: "Nulla porta felis mollis est suscipit vestibulum. Integer fermentum ullamcorper leo a pulvinar." 28 | - 29 | title: "Donec rhoncus facilisis velit, efficitur interdum" 30 | description: "Ut at nunc tristique, tincidunt tortor eget, consequat magna. Phasellus cursus nisi et orci porttitor feugiat. Etiam porttitor consequat sapien eu elementum. Nulla in interdum enim, non molestie tellus. In id sagittis nulla. Morbi ultrices eros libero, quis vehicula mauris egestas vitae. Fusce varius tortor risus. Aliquam id cursus massa." 31 | --- 32 | -------------------------------------------------------------------------------- /src/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/_data/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": [ 3 | { 4 | "caption": "FAQ", 5 | "url": "/faq/", 6 | "external": false 7 | }, 8 | { 9 | "caption": "Changelog", 10 | "url": "/changelog/", 11 | "external": false 12 | }, 13 | { 14 | "caption": "Spruce CSS", 15 | "url": "https://sprucecss.com", 16 | "external": true 17 | } 18 | ], 19 | "footer": [ 20 | { 21 | "title": "Getting Started", 22 | "items": [ 23 | { 24 | "caption": "Introduction", 25 | "url": "/getting-started/introduction/", 26 | "external": false 27 | }, 28 | { 29 | "caption": "Features", 30 | "url": "/getting-started/features/", 31 | "external": false 32 | }, 33 | { 34 | "caption": "Structure", 35 | "url": "/getting-started/structure/", 36 | "external": false 37 | }, 38 | { 39 | "caption": "Setup", 40 | "url": "/getting-started/setup/", 41 | "external": false 42 | } 43 | ] 44 | }, 45 | { 46 | "title": "Spruce CSS", 47 | "items": [ 48 | { 49 | "caption": "Introduction", 50 | "url": "https://sprucecss.com/docs/getting-started/introduction/", 51 | "external": true 52 | }, 53 | { 54 | "caption": "Themes", 55 | "url": "https://sprucecss.com/docs/customization/themes/", 56 | "external": true 57 | }, 58 | { 59 | "caption": "Components", 60 | "url": "https://sprucecss.com/ui/getting-started/introduction/", 61 | "external": true 62 | } 63 | ] 64 | }, 65 | { 66 | "title": "More From Us", 67 | "items": [ 68 | { 69 | "caption": "Services", 70 | "url": "https://conedevelopment.com/services/", 71 | "external": true 72 | }, 73 | { 74 | "caption": "Pine development blog", 75 | "url": "https://pineco.de/", 76 | "external": true 77 | }, 78 | { 79 | "caption": "Root Laravel admin", 80 | "url": "https://root.conedevelopment.com/", 81 | "external": true 82 | }, 83 | { 84 | "caption": "Bite-Sized Accessibility", 85 | "url": "https://bite-sized-a11y.com/", 86 | "external": true 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /src/_includes/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | {% if navigation.header.length > 0 %} 7 | 29 | {% endif %} 30 | 33 |
34 |
35 | {% include "partials/search-toggle.html" %} 36 | {% include "partials/theme-switcher.html" %} 37 |
38 | {% if (site.social.github) %} 39 | 40 | {% svgIcon './src/img/icon/github.svg' %} 41 | 42 | {% endif %} 43 | {% if (site.social.discord) %} 44 | 45 | {% svgIcon './src/img/icon/discord.svg' %} 46 | 47 | {% endif %} 48 | {% if (site.social.twitter) %} 49 | 50 | {% svgIcon './src/img/icon/twitter.svg' %} 51 | 52 | {% endif %} 53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const { execSync } = require('child_process'); 4 | const dateFilter = require('./src/filters/date-filter.js'); 5 | const eleventyNavigationPlugin = require('@11ty/eleventy-navigation'); 6 | const htmlMinTransform = require('./src/transforms/html-min-transform.js'); 7 | const isProduction = process.env.NODE_ENV === 'production'; 8 | const markdownIt = require('markdown-it'); 9 | const markdownItAnchor = require('markdown-it-anchor'); 10 | const pluginTOC = require('eleventy-plugin-toc'); 11 | const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight'); 12 | const w3DateFilter = require('./src/filters/w3-date-filter.js'); 13 | const parentFilter = require('./src/filters/parent-filter.js'); 14 | const markdownRenderShortcode = require('./src/shortcodes/markdown-render.js'); 15 | const svgIconShortcode = require('./src/shortcodes/svg-icon.js'); 16 | 17 | module.exports = config => { 18 | config.addFilter('dateFilter', dateFilter); 19 | config.addFilter('w3DateFilter', w3DateFilter); 20 | config.addFilter('parentFilter', parentFilter); 21 | config.addFilter('debugger', (...args) => { 22 | console.log(...args); 23 | debugger; 24 | }); 25 | 26 | config.addPlugin(eleventyNavigationPlugin); 27 | config.addPlugin(syntaxHighlight); 28 | config.addPlugin(pluginTOC); 29 | 30 | config.addPassthroughCopy({ './src/robots.txt': '/robots.txt' }); 31 | config.addPassthroughCopy('./src/img/**'); 32 | config.addPassthroughCopy('./src/css/**'); 33 | config.addPassthroughCopy('./src/js/**'); 34 | config.addPassthroughCopy('./src/font/**'); 35 | 36 | config.addCollection('posts', collection => { 37 | const items = collection.getFilteredByGlob('./src/posts/**/posts/*.md'); 38 | return items.sort((a, b) => a.data.eleventyNavigation.order - b.data.eleventyNavigation.order); 39 | }); 40 | 41 | config.addAsyncShortcode('svgIcon', svgIconShortcode); 42 | config.addAsyncShortcode('markdownRender', markdownRenderShortcode); 43 | 44 | if (isProduction) { 45 | config.addTransform('htmlmin', htmlMinTransform); 46 | } 47 | 48 | const markdownLib = markdownIt({ html: true }).use( 49 | markdownItAnchor, 50 | { 51 | permalink: true, 52 | permalinkClass: 'anchor', 53 | permalinkSymbol: '#' 54 | } 55 | ); 56 | 57 | config.setLibrary('md', markdownLib); 58 | 59 | config.on('eleventy.after', () => { 60 | execSync(`npx pagefind --source dist --glob \"**/*.html\"`, { encoding: 'utf-8' }) 61 | }); 62 | 63 | return { 64 | markdownTemplateEngine: 'njk', 65 | dataTemplateEngine: 'njk', 66 | htmlTemplateEngine: 'njk', 67 | dir: { 68 | input: 'src', 69 | output: 'dist' 70 | }, 71 | passthroughFileCopy: true, 72 | pathPrefix: './', 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/scss/component/_prism.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | code[class*='language-'], 4 | pre[class*='language-'] { 5 | border-radius: config('border-radius-sm', $display); 6 | color: color('color', 'prism'); 7 | font-family: config('font-family-cursive', $typography); 8 | font-size: config('font-size-base', $typography); 9 | hyphens: none; 10 | line-height: 1.5; 11 | tab-size: 4; 12 | text-align: left; 13 | white-space: pre; 14 | word-break: normal; 15 | word-spacing: normal; 16 | word-wrap: normal; 17 | } 18 | 19 | @media print { 20 | code[class*='language-'], 21 | pre[class*='language-'] { 22 | text-shadow: none; 23 | } 24 | } 25 | 26 | /* Code blocks */ 27 | pre[class*='language-'] { 28 | display: grid; 29 | overflow: auto; 30 | padding: spacer('m'); 31 | } 32 | 33 | pre[class*='language-'] code { 34 | background-color: transparent; 35 | padding: 0; 36 | } 37 | 38 | :not(pre) > code[class*='language-'], 39 | pre[class*='language-'] { 40 | background: color('background', 'prism'); 41 | overflow-x: auto; 42 | } 43 | 44 | .token.comment, 45 | .token.prolog, 46 | .token.cdata { 47 | color: color('comment', 'prism'); 48 | font-style: italic; 49 | } 50 | 51 | .token.punctuation { 52 | color: color('punctuation', 'prism'); 53 | } 54 | 55 | .namespace { 56 | color: color('namespace', 'prism'); 57 | } 58 | 59 | .token.deleted { 60 | color: color('deleted', 'prism'); 61 | font-style: italic; 62 | } 63 | 64 | .token.symbol, 65 | .token.operator, 66 | .token.keyword, 67 | .token.property { 68 | color: color('namespace', 'prism'); 69 | } 70 | 71 | .token.tag { 72 | color: color('punctuation', 'prism'); 73 | } 74 | 75 | .token.boolean { 76 | color: color('boolean', 'prism'); 77 | } 78 | 79 | .token.number { 80 | color: color('number', 'prism'); 81 | } 82 | 83 | .token.constant, 84 | .token.builtin, 85 | .token.string, 86 | .token.url, 87 | .token.entity, 88 | .language-css .token.string, 89 | .style .token.string, 90 | .token.char { 91 | color: color('constant', 'prism'); 92 | } 93 | 94 | .token.selector, 95 | .token.function, 96 | .token.doctype { 97 | color: color('punctuation', 'prism'); 98 | font-style: italic; 99 | } 100 | 101 | .token.attr-name, 102 | .token.inserted { 103 | color: color('constant', 'prism'); 104 | font-style: italic; 105 | } 106 | 107 | .token.class-name, 108 | .token.atrule { 109 | color: color('class-name', 'prism'); 110 | } 111 | 112 | .token.regex, 113 | .token.important, 114 | .token.variable { 115 | color: color('regex', 'prism'); 116 | } 117 | 118 | .token.important, 119 | .token.bold { 120 | font-weight: bold; 121 | } 122 | 123 | .token.italic { 124 | font-style: italic; 125 | } 126 | -------------------------------------------------------------------------------- /src/scss/config/_config.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'dark-colors' as dark; 3 | 4 | $color-primary: hsl(238deg 100% 50%); 5 | $color-secondary: hsl(186deg 100% 60%); 6 | 7 | @use 'sprucecss/scss/spruce' with ( 8 | $btn: ( 9 | 'font-family': #{'Manrope', sans-serif}, 10 | 'font-weight': 700, 11 | 'padding': 0.75em 1.5em, 12 | ), 13 | $btn-lg: ( 14 | 'font-size': 1rem, 15 | 'padding': 1em 1.75em, 16 | ), 17 | $color-black: hsl(245 31% 21%), 18 | $color-primary: $color-primary, 19 | $color-secondary: $color-secondary, 20 | $colors: ( 21 | 'base': ( 22 | 'border': hsl(0deg 0% 0% / 5%), 23 | 'code-background': hsl(224deg 94% 97%), 24 | 'heading': hsl(232deg 68% 11%), 25 | 'text': hsl(208, 21%, 39%), 26 | ), 27 | 'btn': ( 28 | 'secondary-foreground': color.adjust($color-secondary, $lightness: -50%), 29 | 'secondary-shadow': color.adjust($color-secondary, $lightness: 20%), 30 | ), 31 | 'breadcrumb': ( 32 | 'separator': hsl(0deg 0% 80%), 33 | ), 34 | 'card': ( 35 | 'background': hsl(0deg 0% 100%), 36 | ), 37 | 'footer': ( 38 | 'background': hsl(224deg 94% 98%), 39 | ), 40 | 'navigation': ( 41 | 'arrow': hsl(0deg 0% 0% / 15%), 42 | 'icon-background': hsl(224deg 94% 98%), 43 | 'icon-background-hover': $color-primary, 44 | 'icon-foreground': $color-primary, 45 | 'icon-foreground-hover': hsl(0deg 0% 100%), 46 | ), 47 | 'search': ( 48 | 'icon': hsl(229deg 26% 48% / 25%), 49 | ), 50 | 'scrollbar': ( 51 | 'thumb-background': hsl(229deg 26% 48% / 15%), 52 | 'thumb-background-hover': hsl(0deg 0% 0% / 25%), 53 | 'track-background': hsl(226deg 100% 87% / 15%), 54 | ), 55 | 'prism': ( 56 | 'color': hsl(243deg 14% 29%), 57 | 'background': hsl(224deg 94% 99%), 58 | 'comment': hsl(225deg 14% 46%), 59 | 'punctuation': hsl(279deg 50% 53%), 60 | 'namespace': hsl(173deg 100% 24%), 61 | 'deleted': hsl(1deg 83% 63% / 56%), 62 | 'boolean': hsl(0deg 44% 53%), 63 | 'number': hsl(315deg 90% 35%), 64 | 'constant': hsl(221deg 57% 52%), 65 | 'class-name': hsl(0deg 0% 7%), 66 | 'regex': hsl(1deg 48% 59%), 67 | ), 68 | ), 69 | $dark-colors: dark.$colors, 70 | $generators: ( 71 | 'form': ( 72 | 'file-btn': false, 73 | 'form-check': false, 74 | 'form-control': false, 75 | 'form-description': false, 76 | 'form-feedback': false, 77 | 'form-fieldset': false, 78 | 'form-group': false, 79 | 'form-label': false, 80 | 'form-range': false, 81 | 'form-row': false, 82 | 'form-switch': false, 83 | ), 84 | ), 85 | $typography: ( 86 | 'font-family-base': #{'Open Sans', sans-serif}, 87 | 'font-family-heading': #{'Manrope', sans-serif}, 88 | 'font-size-lead': clamp(1.1rem, 2vw, 1.25rem), 89 | ), 90 | $settings: ( 91 | 'css-custom-properties': true, 92 | ), 93 | ); 94 | -------------------------------------------------------------------------------- /src/scss/section/_header.scss: -------------------------------------------------------------------------------- 1 | @use 'sprucecss/scss/spruce' as *; 2 | 3 | .header { 4 | align-items: center; 5 | border-block-end: 1px solid color('border'); 6 | display: flex; 7 | flex-wrap: wrap; 8 | gap: spacer('s') spacer-clamp('m', 'l'); 9 | justify-content: space-between; 10 | order: 2; 11 | padding-block: 1.75rem; 12 | 13 | @include breakpoint('sm') { 14 | order: -1; 15 | } 16 | 17 | &__column { 18 | align-items: center; 19 | display: flex; 20 | flex-grow: 1; 21 | gap: spacer-clamp('m', 'l'); 22 | justify-content: space-between; 23 | 24 | @include breakpoint('sm') { 25 | justify-content: flex-start; 26 | } 27 | } 28 | 29 | &__logo { 30 | display: inline-flex; 31 | 32 | img, 33 | svg { 34 | block-size: 2rem; 35 | display: inline-flex; 36 | inline-size: auto; 37 | } 38 | } 39 | 40 | &__toggle { 41 | @include set-css-variable(( 42 | --border-radius: 1rem 1rem 0 1rem 43 | )); 44 | 45 | @include breakpoint('sm') { 46 | display: none; 47 | } 48 | } 49 | 50 | &__actions { 51 | align-items: center; 52 | display: flex; 53 | flex-wrap: wrap; 54 | gap: spacer-clamp('s', 'm'); 55 | margin-inline-start: auto; 56 | } 57 | 58 | &__navigation { 59 | display: none; 60 | 61 | @include breakpoint('sm') { 62 | display: flex; 63 | } 64 | 65 | ul { 66 | @include clear-list; 67 | align-items: center; 68 | background-color: transparent; 69 | display: flex; 70 | flex-direction: row; 71 | flex-wrap: wrap; 72 | gap: spacer('xs') spacer('m'); 73 | inset: auto; 74 | padding: 0; 75 | position: relative; 76 | 77 | @include breakpoint('lg') { 78 | gap: spacer('xs') spacer('l'); 79 | } 80 | } 81 | 82 | li { 83 | margin-block: 0; 84 | } 85 | 86 | a { 87 | align-items: center; 88 | color: color('heading'); 89 | display: flex; 90 | gap: spacer('xs'); 91 | text-decoration: none; 92 | 93 | &:hover { 94 | color: color('primary'); 95 | } 96 | 97 | &[aria-current='page'] { 98 | font-weight: 700; 99 | } 100 | } 101 | 102 | svg { 103 | --dimension: 0.65em; 104 | block-size: var(--dimension); 105 | color: color('arrow', 'navigation'); 106 | inline-size: var(--dimension); 107 | } 108 | } 109 | 110 | &__socials { 111 | align-items: center; 112 | display: flex; 113 | gap: spacer('s'); 114 | 115 | a { 116 | color: color('heading'); 117 | display: inline-flex; 118 | 119 | &:hover, 120 | &:focus { 121 | color: color('primary'); 122 | } 123 | } 124 | 125 | svg { 126 | --dimension: 1.25rem; 127 | block-size: var(--dimension); 128 | inline-size: var(--dimension); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Thumbnails 8 | ._* 9 | 10 | # Files that might appear in the root of a volume 11 | .DocumentRevisions-V100 12 | .fseventsd 13 | .Spotlight-V100 14 | .TemporaryItems 15 | .Trashes 16 | .VolumeIcon.icns 17 | .com.apple.timemachine.donotpresent 18 | 19 | # Directories potentially created on remote AFP share 20 | .AppleDB 21 | .AppleDesktop 22 | Network Trash Folder 23 | Temporary Items 24 | .apdisk 25 | 26 | ### Node ### 27 | # Logs 28 | logs 29 | *.log 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | lerna-debug.log* 34 | 35 | # Diagnostic reports (https://nodejs.org/api/report.html) 36 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | *.pid.lock 43 | 44 | # Directory for instrumented libs generated by jscoverage/JSCover 45 | lib-cov 46 | 47 | # Coverage directory used by tools like istanbul 48 | coverage 49 | *.lcov 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 55 | .grunt 56 | 57 | # Bower dependency directory (https://bower.io/) 58 | bower_components 59 | 60 | # node-waf configuration 61 | .lock-wscript 62 | 63 | # Compiled binary addons (https://nodejs.org/api/addons.html) 64 | build/Release 65 | 66 | # Dependency directories 67 | node_modules/ 68 | jspm_packages/ 69 | 70 | # TypeScript v1 declaration files 71 | typings/ 72 | 73 | # TypeScript cache 74 | *.tsbuildinfo 75 | 76 | # Optional npm cache directory 77 | .npm 78 | 79 | # Optional eslint cache 80 | .eslintcache 81 | 82 | # Optional stylelint cache 83 | .stylelintcache 84 | 85 | # Microbundle cache 86 | .rpt2_cache/ 87 | .rts2_cache_cjs/ 88 | .rts2_cache_es/ 89 | .rts2_cache_umd/ 90 | 91 | # Optional REPL history 92 | .node_repl_history 93 | 94 | # Output of 'npm pack' 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | .yarn-integrity 99 | 100 | # dotenv environment variables file 101 | .env 102 | .env.test 103 | .env*.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | .cache 107 | .parcel-cache 108 | 109 | # Next.js build output 110 | .next 111 | 112 | # Nuxt.js build / generate output 113 | .nuxt 114 | dist 115 | 116 | # Storybook build outputs 117 | .out 118 | .storybook-out 119 | storybook-static 120 | 121 | # rollup.js default build output 122 | dist/ 123 | 124 | # Gatsby files 125 | .cache/ 126 | # Comment in the public line in if your project uses Gatsby and not Next.js 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | # public 129 | 130 | # vuepress build output 131 | .vuepress/dist 132 | 133 | # Serverless directories 134 | .serverless/ 135 | 136 | # FuseBox cache 137 | .fusebox/ 138 | 139 | # DynamoDB Local files 140 | .dynamodb/ 141 | 142 | # TernJS port file 143 | .tern-port 144 | 145 | # Stores VSCode versions used for testing VSCode extensions 146 | .vscode-test 147 | 148 | # Temporary folders 149 | tmp/ 150 | temp/ 151 | 152 | ### Windows ### 153 | # Windows thumbnail cache files 154 | Thumbs.db 155 | Thumbs.db:encryptable 156 | ehthumbs.db 157 | ehthumbs_vista.db 158 | 159 | # Dump file 160 | *.stackdump 161 | 162 | # Folder config file 163 | [Dd]esktop.ini 164 | 165 | # Recycle Bin used on file shares 166 | $RECYCLE.BIN/ 167 | 168 | # Windows Installer files 169 | *.cab 170 | *.msi 171 | *.msix 172 | *.msm 173 | *.msp 174 | 175 | # Windows shortcuts 176 | *.lnk 177 | -------------------------------------------------------------------------------- /src/scss/config/_dark-colors.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | $dark-color-black: hsl(245deg 38% 11%); 4 | $dark-color-danger: hsl(0deg 71% 51%); 5 | $dark-color-gray-dark: hsl(0deg 0% 100% / 8%); 6 | $dark-color-gray: hsl(0deg 0% 97%); 7 | $dark-color-primary: hsl(186deg 100% 60%); 8 | $dark-color-secondary: hsl(227deg 92% 55%); 9 | $dark-color-success: hsl(150deg 100% 33%); 10 | $dark-color-white: hsl(0deg 0% 95%); 11 | 12 | $colors: ( 13 | 'base': ( 14 | 'background': $dark-color-black, 15 | 'blockquote-border': $dark-color-primary, 16 | 'border': $dark-color-gray-dark, 17 | 'card-border': hsl(207deg 90% 13%), 18 | 'code-background': hsl(207deg 64% 21%), 19 | 'code-foreground': $dark-color-white, 20 | 'footer-background': hsl(0deg 0% 0% / 15%), 21 | 'heading': $dark-color-white, 22 | 'link-hover': color.scale($dark-color-primary, $lightness: 10%), 23 | 'link': $dark-color-primary, 24 | 'mark-background': hsl(50deg 100% 80%), 25 | 'mark-foreground': $dark-color-black, 26 | 'marker': $dark-color-primary, 27 | 'primary': $dark-color-primary, 28 | 'secondary': $dark-color-secondary, 29 | 'text': $dark-color-gray, 30 | ), 31 | 'breadcrumb': ( 32 | 'arrow': hsl(0deg 0% 100% / 0.1), 33 | ), 34 | 'btn': ( 35 | 'primary-background': $dark-color-primary, 36 | 'primary-background-hover': hsl(186deg 100% 45%), 37 | 'primary-foreground': hsl(186deg 100% 5%), 38 | 'primary-shadow': hsl(186deg 100% 25%), 39 | 'secondary-background': $dark-color-secondary, 40 | 'secondary-background-hover': color.adjust($dark-color-secondary, $lightness: 5%), 41 | 'secondary-foreground': $dark-color-white, 42 | 'secondary-shadow': color.adjust($dark-color-secondary, $lightness: -20%), 43 | ), 44 | 'card': ( 45 | 'background': $dark-color-black, 46 | ), 47 | 'container': ( 48 | 'background': hsl(207deg 92% 12%), 49 | ), 50 | 'form': ( 51 | 'background': color.scale($dark-color-black, $lightness: 5%), 52 | 'background-disabled': $dark-color-black, 53 | 'border-disabled': $dark-color-gray-dark, 54 | 'border-focus': $dark-color-primary, 55 | 'border': $dark-color-gray-dark, 56 | 'check-background': $dark-color-primary, 57 | 'check-foreground': $dark-color-black, 58 | 'invalid': $dark-color-danger, 59 | 'invalid-shadow': color.adjust($dark-color-danger, $alpha: -0.75), 60 | 'label': $dark-color-white, 61 | 'legend': $dark-color-white, 62 | 'placeholder': hsl(0deg 0% 60%), 63 | 'select-foreground': hsl(0deg 0% 100%), 64 | 'shadow-focus': color.adjust($dark-color-primary, $alpha: -0.75), 65 | 'text': $dark-color-gray, 66 | 'valid': $dark-color-success, 67 | 'valid-shadow': color.adjust($dark-color-success, $alpha: -0.75), 68 | ), 69 | 'footer': ( 70 | 'background': hsl(245deg 38% 10%), 71 | ), 72 | 'navigation': ( 73 | 'arrow': hsl(0deg 0% 100% / 15%), 74 | 'icon-background': hsl(245deg 38% 10%), 75 | 'icon-background-hover': $dark-color-primary, 76 | 'icon-foreground': $dark-color-primary, 77 | 'icon-foreground-hover': hsl(186deg 100% 5%), 78 | ), 79 | 'prism': ( 80 | 'color': hsl(217deg 34% 88%), 81 | 'background': hsl(245deg 38% 7%), 82 | 'comment': hsl(180deg 9% 55%), 83 | 'punctuation': hsl(276deg 68% 75%), 84 | 'namespace': hsl(197deg 31% 77%), 85 | 'deleted': hsl(1deg 83% 63% / 56%), 86 | 'boolean': hsl(350deg 100% 67%), 87 | 'number': hsl(14deg 90% 70%), 88 | 'constant': hsl(221deg 100% 75%), 89 | 'class-name': hsl(33deg 100% 77%), 90 | 'regex': hsl(217deg 34% 88%), 91 | ), 92 | 'table': ( 93 | 'border': $dark-color-gray-dark, 94 | 'caption': $dark-color-gray, 95 | 'heading': $dark-color-white, 96 | 'hover': hsl(0deg 0% 100% / 5%), 97 | 'stripe': hsl(0deg 0% 100% / 2.5%), 98 | 'text': $dark-color-gray, 99 | ), 100 | 'selection': ( 101 | 'background': $dark-color-primary, 102 | 'foreground': hsl(186deg 100% 5%), 103 | ), 104 | 'search': ( 105 | 'icon': hsl(0deg 0% 100% / 25%), 106 | ), 107 | 'scrollbar': ( 108 | 'thumb-background': hsl(0 0% 100% / 15%), 109 | 'thumb-background-hover': hsl(0 0% 100% / 25%), 110 | 'track-background': hsl(0 0% 100% / 5%), 111 | ), 112 | ); 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 24 | * Focusing on what is best not just for us as individuals, but for the overall community 25 | 26 | Examples of unacceptable behavior include: 27 | 28 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 29 | * Trolling, insulting or derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or email address, without their explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a professional setting 33 | 34 | ## Enforcement Responsibilities 35 | 36 | Community leaders are responsible for clarifying and enforcing our standards of 37 | acceptable behavior and will take appropriate and fair corrective action in 38 | response to any behavior that they deem inappropriate, threatening, offensive, 39 | or harmful. 40 | 41 | Community leaders have the right and responsibility to remove, edit, or reject 42 | comments, commits, code, wiki edits, issues, and other contributions that are 43 | not aligned to this Code of Conduct, and will communicate reasons for moderation 44 | decisions when appropriate. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all community spaces, and also applies when 49 | an individual is officially representing the community in public spaces. 50 | Examples of representing our community include using an official e-mail address, 51 | posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. 53 | 54 | ## Enforcement 55 | 56 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 57 | reported to the community leaders responsible for enforcement at 58 | hello@conedevelopment.com. 59 | All complaints will be reviewed and investigated promptly and fairly. 60 | 61 | All community leaders are obligated to respect the privacy and security of the 62 | reporter of any incident. 63 | 64 | ## Enforcement Guidelines 65 | 66 | Community leaders will follow these Community Impact Guidelines in determining 67 | the consequences for any action they deem in violation of this Code of Conduct: 68 | 69 | ### 1. Correction 70 | 71 | **Community Impact**: Use of inappropriate language or other behavior deemed 72 | unprofessional or unwelcome in the community. 73 | 74 | **Consequence**: A private, written warning from community leaders, providing 75 | clarity around the nature of the violation and an explanation of why the 76 | behavior was inappropriate. A public apology may be requested. 77 | 78 | ### 2. Warning 79 | 80 | **Community Impact**: A violation through a single incident or series 81 | of actions. 82 | 83 | **Consequence**: A warning with consequences for continued behavior. No 84 | interaction with the people involved, including unsolicited interaction with 85 | those enforcing the Code of Conduct, for a specified period of time. This 86 | includes avoiding interactions in community spaces as well as external channels 87 | like social media. Violating these terms may lead to a temporary or 88 | permanent ban. 89 | 90 | ### 3. Temporary Ban 91 | 92 | **Community Impact**: A serious violation of community standards, including 93 | sustained inappropriate behavior. 94 | 95 | **Consequence**: A temporary ban from any sort of interaction or public 96 | communication with the community for a specified period of time. No public or 97 | private interaction with the people involved, including unsolicited interaction 98 | with those enforcing the Code of Conduct, is allowed during this period. 99 | Violating these terms may lead to a permanent ban. 100 | 101 | ### 4. Permanent Ban 102 | 103 | **Community Impact**: Demonstrating a pattern of violation of community 104 | standards, including sustained inappropriate behavior, harassment of an 105 | individual, or aggression toward or disparagement of classes of individuals. 106 | 107 | **Consequence**: A permanent ban from any sort of public interaction within 108 | the community. 109 | 110 | ## Attribution 111 | 112 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 113 | version 2.0, available at 114 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 115 | 116 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 117 | enforcement ladder](https://github.com/mozilla/diversity). 118 | 119 | [homepage]: https://www.contributor-covenant.org 120 | 121 | For answers to common questions about this code of conduct, see the FAQ at 122 | https://www.contributor-covenant.org/faq. Translations are available at 123 | https://www.contributor-covenant.org/translations. 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | 6 | 7 | Spruce CSS 8 | 9 |
10 |
11 |

12 | 13 | **Welcome to the official documentation of **Spruce Docs** Elventy theme. A small template you can use to document any of your projects.** 14 | 15 |
16 | 17 | ![The preview image of the theme.](./.github/spruce-docs-preview-mockup-2.png) 18 | 19 | 20 | [![Github release](https://img.shields.io/github/v/release/conedevelopment/sprucecss-eleventy-documentation-template?color=2350f6&logo=github&logoColor=white&style=for-the-badge)](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/releases/latest) 21 | [![License](https://img.shields.io/badge/license-MIT-2350f6?style=for-the-badge)](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/LICENSE) 22 | 23 | A documentation template is always helpful. There are a lot of solutions to make one; we wanted to create our self-hosted version based on our favorite static site generator [Eleventy](https://www.11ty.dev/). 24 | 25 | By structure, it is simple, with two levels and additional custom templates like [FAQ]([/faq/](https://eleventy-documentation.sprucecss.com/faq/)) and [Changelog]([/changelog/](https://eleventy-documentation.sprucecss.com/changelog/)). 26 | 27 | ## Spruce CSS 28 | 29 | The template is built on [Spruce CSS](https://sprucecss.com/), a small and customizable CSS framework. The main benefit of this is that you can use the Spruce UI components with dark mode and RTL support. 30 | 31 | ## Features 32 | 33 | - Breadcrumb navigation built on [11ty Navigation Plugin](https://www.11ty.dev/docs/plugins/navigation/). 34 | - HTML minification in production mode. 35 | - Anchor headings. 36 | - Table of Content. 37 | - FAQ template. 38 | - Changelog template. 39 | - Static search integration with [pagefind](https://pagefind.app/). 40 | - Code highlighting. 41 | - RTL support. 42 | - Dark theme mode. 43 | - [svgIcon](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/src/shortcodes/svg-icon.js) shortcode: render any SVG icon inline and add optional classes. 44 | - [markdownRenderer](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/src/shortcodes/markdown-render.js): render any string (markdown) into HTML. 45 | 46 | ## Setup 47 | 48 | 1. **Clone the repository.** 49 | 50 | 2. **Install the dependencies.** 51 | 52 | In the `package.json` file, you will find all of the dependencies (and scripts) to install them using the following command: 53 | 54 | ```shell 55 | npm install 56 | ``` 57 | 58 | 3. **Run the development mode** 59 | 60 | To run the development mode, use the `npm script`. This script will also watch for changes. 61 | 62 | ```shell 63 | npm start 64 | ``` 65 | 66 | 4. **Run the production mode** 67 | 68 | Before you go live, you should use the production script to compress the Sass files. 69 | 70 | ```shell 71 | npm run prod 72 | ``` 73 | 74 | You can find some more npm scripts in the [package.json](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template/blob/main/package.json) that can be helpful. 75 | 76 | ## Content Managment 77 | 78 | Adding content to the template is easy as almost everything is in Eleventy. 79 | 80 | ### The Basic Structure 81 | 82 | Our base folder for the documentation pages is the `posts` folder. You must follow the folder structure, which means the `category` here. If you create a folder, you must make a list page with the same name as the folder. You must also create another `posts` folder under the `category` folder where your posts go. You must create the `posts.json` file that will parameter your `layout` and `permalink` values. 83 | 84 | ### Eleventy Navigation 85 | 86 | The theme utilizes the [Eleventy Navigation plugin](https://www.11ty.dev/docs/plugins/navigation/), so you must explicitly set up the hierarchy. This is needed for the automatic sidebar navigation, the navigation order, and breadcrumb generation. 87 | 88 | ### Other Pages 89 | 90 | To create simple pages, make a file directly under the `src` folder and configure it with the available front matter. 91 | 92 | ## Structure 93 | 94 | ```html 95 | spruecss-eleventy-documentation-template/ 96 | ├─ node_modules/ 97 | ├─ dist/ 98 | ├─ src/ 99 | │ ├─ _data/ 100 | │ ├─ _includes/ 101 | │ ├─ css/ 102 | │ ├─ filters/ 103 | │ ├─ font/ 104 | │ ├─ img/ 105 | │ ├─ js/ 106 | │ ├─ posts/ 107 | │ ├─ scss/ 108 | │ ├─ shortcodes/ 109 | │ ├─ transforms/ 110 | │ ├─ changelog.md 111 | │ ├─ faq.md 112 | │ ├─ index.md 113 | ├─ .eleventy.js 114 | ├─ package.json 115 | ├─ README.md 116 | ├─ ... 117 | 118 | ``` 119 | 120 | - **_data**: Some global data, like the name of your site and helpers like the active navigation element or current year. 121 | - **__includes**: All of the layout and partial templates. 122 | - **css**: The compiled CSS. 123 | - **filters**: The additional filters that you can use. 124 | - **font**: The custom fonts. 125 | - **img**: The static image files. 126 | - **posts**: The markdown contents. 127 | - **scss**: The Sass files. 128 | - **shortcodes**: The available shortcodes. 129 | - **transforms**: The transformations. 130 | 131 |
132 | 133 | [![Netlify Status](https://api.netlify.com/api/v1/badges/b7560c95-7035-491b-8c3f-94c43bea761e/deploy-status)](https://app.netlify.com/sites/sprucecss-eleventy-documentation/deploys) 134 | -------------------------------------------------------------------------------- /src/img/cone-docs-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/spruce-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/spruce-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @font-face{font-display:swap;font-family:"Manrope";font-style:normal;font-weight:400;src:url("../../font/manrope-v14-latin-regular.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Manrope";font-style:normal;font-weight:600;src:url("../../font/manrope-v14-latin-600.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Manrope";font-style:normal;font-weight:700;src:url("../../font/manrope-v14-latin-800.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:400;src:url("../../font/open-sans-v35-latin-regular.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:700;src:url("../../font/open-sans-v35-latin-700.woff2") format("woff2")}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;block-size:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-inline-size:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{block-size:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{--spruce-alert-color-danger: hsl(0, 71%, 51%);--spruce-alert-color-info: hsl(195, 100%, 42%);--spruce-alert-color-success: hsl(150, 100%, 33%);--spruce-alert-color-warning: hsl(48, 89%, 55%)}:root{--spruce-base-color-background: hsl(0, 0%, 100%);--spruce-base-color-blockquote-border: hsl(238, 100%, 50%);--spruce-base-color-border: hsla(0, 0%, 0%, 0.05);--spruce-base-color-code-background: hsl(224, 94%, 97%);--spruce-base-color-code-foreground: hsl(245, 31%, 21%);--spruce-base-color-heading: hsl(232, 68%, 11%);--spruce-base-color-link: hsl(238, 100%, 50%);--spruce-base-color-link-hover: #0007cc;--spruce-base-color-mark-background: hsl(50, 100%, 80%);--spruce-base-color-mark-foreground: hsl(245, 31%, 21%);--spruce-base-color-marker: hsl(238, 100%, 50%);--spruce-base-color-primary: hsl(238, 100%, 50%);--spruce-base-color-secondary: hsl(186, 100%, 60%);--spruce-base-color-strong: hsl(245, 31%, 21%);--spruce-base-color-text: hsl(208, 21%, 39%)}:root{--spruce-btn-color-primary-background: hsl(238, 100%, 50%);--spruce-btn-color-primary-background-hover: #0007cc;--spruce-btn-color-primary-foreground: hsl(0, 0%, 100%);--spruce-btn-color-primary-shadow: #b3b5ff;--spruce-btn-color-secondary-background: hsl(186, 100%, 60%);--spruce-btn-color-secondary-background-hover: #00e6ff;--spruce-btn-color-secondary-foreground: #002e33;--spruce-btn-color-secondary-shadow: #99f5ff}:root{--spruce-form-color-background: hsl(0, 0%, 100%);--spruce-form-color-background-disabled: hsl(0, 0%, 95%);--spruce-form-color-border: hsl(260, 4%, 70%);--spruce-form-color-border-disabled: hsl(215, 63%, 93%);--spruce-form-color-border-focus: hsl(238, 100%, 50%);--spruce-form-color-check-background: hsl(238, 100%, 50%);--spruce-form-color-check-focus-ring: hsl(238, 100%, 50%);--spruce-form-color-check-foreground: hsl(0, 0%, 100%);--spruce-form-color-group-label-background: hsl(210, 60%, 98%);--spruce-form-color-group-label-foreground: hsl(208, 9%, 42%);--spruce-form-color-invalid: hsl(0, 71%, 51%);--spruce-form-color-invalid-focus-ring: rgba(219, 41, 41, 0.25);--spruce-form-color-label: hsl(245, 31%, 21%);--spruce-form-color-legend: hsl(245, 31%, 21%);--spruce-form-color-placeholder: hsl(208, 7%, 40%);--spruce-form-color-range-thumb-background: hsl(238, 100%, 50%);--spruce-form-color-range-thumb-focus-ring: hsl(238, 100%, 50%);--spruce-form-color-range-track-background: hsl(215, 63%, 93%);--spruce-form-color-ring-focus: rgba(0, 9, 255, 0.25);--spruce-form-color-select-foreground: hsl(245, 31%, 21%);--spruce-form-color-text: hsl(208, 9%, 42%);--spruce-form-color-valid: hsl(150, 100%, 33%);--spruce-form-color-valid-focus-ring: rgba(0, 168, 84, 0.25)}:root{--spruce-selection-color-foreground: hsl(0, 0%, 100%);--spruce-selection-color-background: hsl(238, 100%, 50%)}:root{--spruce-scrollbar-color-thumb-background: hsla(229, 26%, 48%, 0.15);--spruce-scrollbar-color-thumb-background-hover: hsla(0, 0%, 0%, 0.25);--spruce-scrollbar-color-track-background: hsla(226, 100%, 87%, 0.15)}:root{--spruce-table-color-border: hsl(215, 63%, 93%);--spruce-table-color-caption: hsl(208, 9%, 42%);--spruce-table-color-heading: hsl(245, 31%, 21%);--spruce-table-color-hover: hsla(0, 0%, 0%, 0.05);--spruce-table-color-stripe: hsla(0, 0%, 0%, 0.025);--spruce-table-color-text: hsl(208, 9%, 42%)}:root{--spruce-breadcrumb-color-separator: hsl(0, 0%, 80%)}:root{--spruce-card-color-background: hsl(0, 0%, 100%)}:root{--spruce-footer-color-background: hsl(224, 94%, 98%)}:root{--spruce-navigation-color-arrow: hsla(0, 0%, 0%, 0.15);--spruce-navigation-color-icon-background: hsl(224, 94%, 98%);--spruce-navigation-color-icon-background-hover: hsl(238, 100%, 50%);--spruce-navigation-color-icon-foreground: hsl(238, 100%, 50%);--spruce-navigation-color-icon-foreground-hover: hsl(0, 0%, 100%)}:root{--spruce-search-color-icon: hsla(229, 26%, 48%, 0.25)}:root{--spruce-prism-color-color: hsl(243, 14%, 29%);--spruce-prism-color-background: hsl(224, 94%, 99%);--spruce-prism-color-comment: hsl(225, 14%, 46%);--spruce-prism-color-punctuation: hsl(279, 50%, 53%);--spruce-prism-color-namespace: hsl(173, 100%, 24%);--spruce-prism-color-deleted: hsla(1, 83%, 63%, 0.56);--spruce-prism-color-boolean: hsl(0, 44%, 53%);--spruce-prism-color-number: hsl(315, 90%, 35%);--spruce-prism-color-constant: hsl(221, 57%, 52%);--spruce-prism-color-class-name: hsl(0, 0%, 7%);--spruce-prism-color-regex: hsl(1, 48%, 59%)}:root{--spruce-border-radius: 0.425rem;--spruce-font-family-base: Open Sans, sans-serif;--spruce-font-family-cursive: ui-monospace, Cascadia Code, Source Code Pro, Menlo, Consolas, DejaVu Sans Mono, monospace;--spruce-font-family-heading: Manrope, sans-serif;--spruce-font-size-base: 1rem;--spruce-font-size-lead: clamp(1.1rem, 2vw, 1.25rem);--spruce-font-size-lg: 1.125rem;--spruce-font-size-ratio: 1.25;--spruce-font-size-sm: 0.875rem;--spruce-font-weight-heading: 700;--spruce-inline-padding: 0.1em 0.3em;--spruce-line-height-base: 1.8;--spruce-line-height-heading: calc(2px + 2ex + 2px);--spruce-line-height-lg: 1.8;--spruce-line-height-md: 1.5;--spruce-line-height-sm: 1.2;--spruce-border-radius-lg: 0.925rem;--spruce-border-radius-sm: 0.425rem;--spruce-container-inline-size: 84rem;--spruce-page-margin: 2cm;--spruce-hidden-elements: header, footer, aside, nav, form, iframe, [class^="aspect-ratio"]}@media(prefers-reduced-motion: no-preference){:root{--spruce-duration: 0.15s;--spruce-timing-function: ease-in-out}}.sr-only{block-size:1px !important;border:0 !important;clip:rect(0, 0, 0, 0) !important;inline-size:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;white-space:nowrap !important}[tabindex="-1"]:focus{outline:none !important}::selection{background-color:var(--spruce-selection-color-background);color:var(--spruce-selection-color-foreground);text-shadow:none}html{box-sizing:border-box}@media(prefers-reduced-motion: no-preference){html{scroll-behavior:smooth}}*,::before,::after{box-sizing:inherit}body{background:var(--spruce-base-color-background);color:var(--spruce-base-color-text)}a{color:var(--spruce-base-color-link);text-decoration:underline;transition-duration:var(--spruce-duration);transition-property:color;transition-timing-function:var(--spruce-timing-function)}a:hover{color:var(--spruce-base-color-link-hover)}button{color:inherit}a,button{touch-action:manipulation}hr{border:0;border-block-start:1px solid var(--spruce-base-color-border)}img{block-size:auto;display:block;max-inline-size:100%;user-select:none}iframe{block-size:100%;display:block;inline-size:100%}figure{margin-inline:0}figure figcaption{margin-block-start:.5rem;text-align:center}.table-responsive{--inline-size: 40rem;-webkit-overflow-scrolling:touch;overflow-x:auto}.table-responsive table{min-inline-size:var(--inline-size)}.table{--spruce-line-height: 1.5;--spruce-padding: 1rem;--spruce-responsive-inline-size: 40rem;border-collapse:collapse;color:var(--spruce-table-color-text);inline-size:100%}.table caption{color:var(--spruce-table-color-caption);margin-block-end:1rem}.table th,.table td{border-block-end:1px solid var(--spruce-table-color-border);line-height:var(--spruce-line-height);padding:var(--spruce-padding)}.table th{color:var(--spruce-table-color-heading);text-align:inherit;text-align:-webkit-match-parent}.table--striped>tbody>tr:nth-child(odd){background-color:var(--spruce-table-color-stripe)}.table--hover>tbody>tr:hover{background:var(--spruce-table-color-hover)}.table--clear-border th,.table--clear-border td{border:0}.table--in-line th:first-child,.table--in-line td:first-child{padding-inline-start:0}.table--in-line th:last-child,.table--in-line td:last-child{padding-inline-end:0}.table--sm{--spruce-padding: 0.5rem}.table--sm th,.table--sm td{padding:var(--spruce-padding)}.table--rounded th:first-child,.table--rounded td:first-child{border-end-start-radius:var(--spruce-border-radius-sm);border-start-start-radius:var(--spruce-border-radius-sm)}.table--rounded th:last-child,.table--rounded td:last-child{border-end-end-radius:var(--spruce-border-radius-sm);border-start-end-radius:var(--spruce-border-radius-sm)}html{-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:var(--spruce-font-family-base);font-size:var(--spruce-font-size-base);line-height:var(--spruce-line-height-base)}p,li,h1,h2,h3,h4,h5,h6{hyphens:auto;overflow-wrap:break-word}h1,h2,h3,h4,h5,h6{color:var(--spruce-base-color-heading);font-family:var(--spruce-font-family-heading);font-weight:var(--spruce-font-weight-heading);line-height:var(--spruce-line-height-heading)}h1{font-size:clamp(2.0751953125rem, 2vw + 1rem, 2.44140625rem)}h2{font-size:clamp(1.66015625rem, 2vw + 1rem, 1.953125rem)}h3{font-size:clamp(1.328125rem, 2vw + 1rem, 1.5625rem)}h4{font-size:clamp(1.0625rem, 2vw + 1rem, 1.25rem)}h5{font-size:1rem}h6{font-size:1rem}ul,ol{list-style-position:inside}ul>*,ol>*{margin-block-end:0;margin-block-start:0}ul>*+*,ol>*+*{margin-block-start:.25rem}ul li,ol li{list-style-position:outside}ul li::marker,ol li::marker{color:var(--spruce-base-color-marker)}li>ul,li>ol{margin-block-start:.25rem}dl dt{color:var(--spruce-base-color-heading);font-weight:bold}dl dd{margin:0}dl dd+dt{margin-block-start:1rem}.quote{border-inline-start:.5rem solid var(--spruce-base-color-blockquote-border);padding-inline-start:1.5rem}.quote>*{margin-block-end:0;margin-block-start:0}.quote>*+*{margin-block-start:.5rem}.quote blockquote{border-inline-start:0;padding-inline-start:0}.quote figcaption{text-align:start}blockquote{border-inline-start:.5rem solid var(--spruce-base-color-blockquote-border);margin-inline-start:0;padding-inline-start:1.5rem}blockquote>*{margin-block-end:0;margin-block-start:0}blockquote>*+*{margin-block-start:.5rem}abbr[title]{border-block-end:1px dotted;cursor:help;text-decoration:none}mark{background-color:var(--spruce-base-color-mark-background);border-radius:var(--spruce-border-radius);color:var(--spruce-base-color-mark-foreground);padding:var(--spruce-inline-padding)}code,kbd,samp{background-color:var(--spruce-base-color-code-background);border-radius:var(--spruce-border-radius);color:var(--spruce-base-color-code-foreground);padding:var(--spruce-inline-padding)}strong{color:var(--spruce-base-color-strong)}.lead{font-size:var(--spruce-font-size-lead)}.hidden,[hidden]{display:none !important}.h1{font-size:clamp(2.0751953125rem, 2vw + 1rem, 2.44140625rem)}.h2{font-size:clamp(1.66015625rem, 2vw + 1rem, 1.953125rem)}.h3{font-size:clamp(1.328125rem, 2vw + 1rem, 1.5625rem)}.h4{font-size:clamp(1.0625rem, 2vw + 1rem, 1.25rem)}.h5{font-size:1rem}.h6{font-size:1rem}.btn{--spruce-border-radius: 0.425rem;--spruce-border-width: 1px;--spruce-font-family: Manrope, sans-serif;--spruce-font-size: 1rem;--spruce-font-weight: 700;--spruce-gap: 0.5rem;--spruce-icon-padding: 0.75em;--spruce-icon-size: 1em;--spruce-padding: 0.75em 1.5em;--spruce-shadow-size: 0.25rem;align-items:center;border-radius:var(--spruce-border-radius);border-style:solid;border-width:var(--spruce-border-width);cursor:pointer;display:inline-flex;font-family:var(--spruce-font-family);font-size:var(--spruce-font-size);font-weight:var(--spruce-font-weight);gap:var(--spruce-gap);justify-content:center;line-height:1;padding:var(--spruce-padding);text-align:start;text-decoration:none;transition-duration:var(--spruce-duration);transition-property:background-color,border-color,box-shadow,color;transition-timing-function:var(--spruce-timing-function)}.btn:focus{outline-color:rgba(0,0,0,0);outline-style:solid}.btn:disabled{opacity:.5;pointer-events:none}.btn--icon{padding:var(--spruce-icon-padding)}.btn--icon.btn--sm{padding:var(--spruce-icon-padding)}.btn--icon.btn--lg{padding:var(--spruce-icon-padding)}.btn__icon{block-size:var(--spruce-icon-size);flex-shrink:0;inline-size:var(--spruce-icon-size);pointer-events:none}.btn__icon--sm{block-size:var(--spruce-icon-size);inline-size:var(--spruce-icon-size)}.btn--sm{--spruce-font-size: 0.8rem;--spruce-gap: 0.25rem;--spruce-icon-padding: 0.5em;--spruce-icon-size: 0.8rem;--spruce-padding: 0.5em 0.75em;font-size:var(--spruce-font-size);gap:var(--spruce-gap);padding:var(--spruce-padding)}.btn--lg{--spruce-font-size: 1rem;--spruce-gap: 0.5rem;--spruce-icon-padding: 0.9em;--spruce-padding: 1em 1.75em}.btn--block{inline-size:100%}.btn--primary{background-color:var(--spruce-btn-color-primary-background);border-color:var(--spruce-btn-color-primary-background);color:var(--spruce-btn-color-primary-foreground)}.btn--primary:focus-visible{outline:2px solid var(--spruce-btn-color-primary-background);outline-offset:2px}.btn--primary:hover{background-color:var(--spruce-btn-color-primary-background-hover);border-color:var(--spruce-btn-color-primary-background-hover);color:var(--spruce-btn-color-primary-foreground)}.btn--primary-shadow{box-shadow:0 .55em 1em -0.2em var(--spruce-btn-color-primary-shadow),0 .15em .35em -0.185em var(--spruce-btn-color-primary-shadow)}.btn--secondary{background-color:var(--spruce-btn-color-secondary-background);border-color:var(--spruce-btn-color-secondary-background);color:var(--spruce-btn-color-secondary-foreground)}.btn--secondary:focus-visible{outline:2px solid var(--spruce-btn-color-secondary-background);outline-offset:2px}.btn--secondary:hover{background-color:var(--spruce-btn-color-secondary-background-hover);border-color:var(--spruce-btn-color-secondary-background-hover);color:var(--spruce-btn-color-secondary-foreground)}.btn--secondary-shadow{box-shadow:0 .55em 1em -0.2em var(--spruce-btn-color-secondary-shadow),0 .15em .35em -0.185em var(--spruce-btn-color-secondary-shadow)}.btn--outline-primary{background-color:rgba(0,0,0,0);border-color:var(--spruce-btn-color-primary-background);color:var(--spruce-btn-color-primary-background)}.btn--outline-primary:focus-visible{outline:2px solid var(--spruce-btn-color-primary-background);outline-offset:2px}.btn--outline-primary:hover{background-color:var(--spruce-btn-color-primary-background);border-color:var(--spruce-btn-color-primary-background);color:var(--spruce-btn-color-primary-foreground)}.btn--outline-secondary{background-color:rgba(0,0,0,0);border-color:var(--spruce-btn-color-secondary-background);color:var(--spruce-btn-color-secondary-background)}.btn--outline-secondary:focus-visible{outline:2px solid var(--spruce-btn-color-secondary-background);outline-offset:2px}.btn--outline-secondary:hover{background-color:var(--spruce-btn-color-secondary-background);border-color:var(--spruce-btn-color-secondary-background);color:var(--spruce-btn-color-secondary-foreground)}.form-group-label{--spruce-border-radius: 0.425rem;--spruce-border-width: 1px;align-items:center;background-color:var(--spruce-form-color-group-label-background);border:var(--spruce-border-width) solid var(--spruce-form-color-border);border-radius:var(--spruce-border-radius);color:var(--spruce-form-color-group-label-foreground);display:flex;padding-inline:1rem}:root[data-theme-mode=dark]{--spruce-base-color-background: hsl(245, 38%, 11%);--spruce-base-color-blockquote-border: hsl(186, 100%, 60%);--spruce-base-color-border: hsla(0, 0%, 100%, 0.08);--spruce-base-color-card-border: hsl(207, 90%, 13%);--spruce-base-color-code-background: hsl(207, 64%, 21%);--spruce-base-color-code-foreground: hsl(0, 0%, 95%);--spruce-base-color-footer-background: hsla(0, 0%, 0%, 0.15);--spruce-base-color-heading: hsl(0, 0%, 95%);--spruce-base-color-link-hover: #47edff;--spruce-base-color-link: hsl(186, 100%, 60%);--spruce-base-color-mark-background: hsl(50, 100%, 80%);--spruce-base-color-mark-foreground: hsl(245, 38%, 11%);--spruce-base-color-marker: hsl(186, 100%, 60%);--spruce-base-color-primary: hsl(186, 100%, 60%);--spruce-base-color-secondary: hsl(227, 92%, 55%);--spruce-base-color-text: hsl(0, 0%, 97%)}:root[data-theme-mode=dark]{--spruce-breadcrumb-color-arrow: hsla(0, 0%, 100%, 0.1)}:root[data-theme-mode=dark]{--spruce-btn-color-primary-background: hsl(186, 100%, 60%);--spruce-btn-color-primary-background-hover: hsl(186, 100%, 45%);--spruce-btn-color-primary-foreground: hsl(186, 100%, 5%);--spruce-btn-color-primary-shadow: hsl(186, 100%, 25%);--spruce-btn-color-secondary-background: hsl(227, 92%, 55%);--spruce-btn-color-secondary-background-hover: #3b64f7;--spruce-btn-color-secondary-foreground: hsl(0, 0%, 95%);--spruce-btn-color-secondary-shadow: #072bab}:root[data-theme-mode=dark]{--spruce-card-color-background: hsl(245, 38%, 11%)}:root[data-theme-mode=dark]{--spruce-container-color-background: hsl(207, 92%, 12%)}:root[data-theme-mode=dark]{--spruce-form-color-background: #1b1836;--spruce-form-color-background-disabled: hsl(245, 38%, 11%);--spruce-form-color-border-disabled: hsla(0, 0%, 100%, 0.08);--spruce-form-color-border-focus: hsl(186, 100%, 60%);--spruce-form-color-border: hsla(0, 0%, 100%, 0.08);--spruce-form-color-check-background: hsl(186, 100%, 60%);--spruce-form-color-check-foreground: hsl(245, 38%, 11%);--spruce-form-color-invalid: hsl(0, 71%, 51%);--spruce-form-color-invalid-shadow: rgba(219, 41, 41, 0.25);--spruce-form-color-label: hsl(0, 0%, 95%);--spruce-form-color-legend: hsl(0, 0%, 95%);--spruce-form-color-placeholder: hsl(0, 0%, 60%);--spruce-form-color-select-foreground: hsl(0, 0%, 100%);--spruce-form-color-shadow-focus: rgba(51, 235, 255, 0.25);--spruce-form-color-text: hsl(0, 0%, 97%);--spruce-form-color-valid: hsl(150, 100%, 33%);--spruce-form-color-valid-shadow: rgba(0, 168, 84, 0.25)}:root[data-theme-mode=dark]{--spruce-footer-color-background: hsl(245, 38%, 10%)}:root[data-theme-mode=dark]{--spruce-navigation-color-arrow: hsla(0, 0%, 100%, 0.15);--spruce-navigation-color-icon-background: hsl(245, 38%, 10%);--spruce-navigation-color-icon-background-hover: hsl(186, 100%, 60%);--spruce-navigation-color-icon-foreground: hsl(186, 100%, 60%);--spruce-navigation-color-icon-foreground-hover: hsl(186, 100%, 5%)}:root[data-theme-mode=dark]{--spruce-prism-color-color: hsl(217, 34%, 88%);--spruce-prism-color-background: hsl(245, 38%, 7%);--spruce-prism-color-comment: hsl(180, 9%, 55%);--spruce-prism-color-punctuation: hsl(276, 68%, 75%);--spruce-prism-color-namespace: hsl(197, 31%, 77%);--spruce-prism-color-deleted: hsla(1, 83%, 63%, 0.56);--spruce-prism-color-boolean: hsl(350, 100%, 67%);--spruce-prism-color-number: hsl(14, 90%, 70%);--spruce-prism-color-constant: hsl(221, 100%, 75%);--spruce-prism-color-class-name: hsl(33, 100%, 77%);--spruce-prism-color-regex: hsl(217, 34%, 88%)}:root[data-theme-mode=dark]{--spruce-table-color-border: hsla(0, 0%, 100%, 0.08);--spruce-table-color-caption: hsl(0, 0%, 97%);--spruce-table-color-heading: hsl(0, 0%, 95%);--spruce-table-color-hover: hsla(0, 0%, 100%, 0.05);--spruce-table-color-stripe: hsla(0, 0%, 100%, 0.025);--spruce-table-color-text: hsl(0, 0%, 97%)}:root[data-theme-mode=dark]{--spruce-selection-color-background: hsl(186, 100%, 60%);--spruce-selection-color-foreground: hsl(186, 100%, 5%)}:root[data-theme-mode=dark]{--spruce-search-color-icon: hsla(0, 0%, 100%, 0.25)}:root[data-theme-mode=dark]{--spruce-scrollbar-color-thumb-background: hsla(0, 0%, 100%, 0.15);--spruce-scrollbar-color-thumb-background-hover: hsla(0, 0%, 100%, 0.25);--spruce-scrollbar-color-track-background: hsla(0, 0%, 100%, 0.05)}[data-theme-mode=dark]{color-scheme:dark}[data-theme-mode=dark] select.form-control:not([multiple]):not([size]){background-image:url('data:image/svg+xml,%3csvg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"%3e%3cpath d="M12,12.507l-3.816,-3.815c-0.171,-0.172 -0.45,-0.172 -0.622,-0l-0.933,0.933c-0.172,0.172 -0.172,0.451 0,0.623l5.06,5.06c0.172,0.172 0.45,0.172 0.622,0l5.06,-5.06c0.172,-0.172 0.172,-0.451 -0,-0.623l-0.933,-0.933c-0.172,-0.172 -0.451,-0.172 -0.622,-0l-3.816,3.815Z" style="fill:hsl%280, 0%, 100%%29;"/%3e%3c/svg%3e')}.container{--inline-size: var(--spruce-container-inline-size);--gap: var(--spruce-container-gap);inline-size:100%;margin-inline:auto;max-inline-size:var(--inline-size);padding-inline:var(--gap)}.container--wide{--inline-size: 94rem}.container--narrow{--inline-size: 50rem}.l-main{--gtc: minmax(0, 1fr);display:grid;gap:3rem;grid-template-columns:var(--gtc);margin-block:clamp(1.5rem, 5vw, 3rem) clamp(3rem, 5vw, 4.5rem)}@media(min-width: 48em){.l-main{--gtc: minmax(0, 16rem) minmax(0, 1fr);gap:calc(var(--spruce-container-gap) * 1.5)}}@media(min-width: 64em){.l-main{--gtc: minmax(0, 18.5rem) minmax(0, 1fr)}}@media(min-width: 80em){.l-main{--gtc: minmax(0, 20rem) minmax(0, 1fr)}}.l-main__sidebar{display:none !important}@media(min-width: 48em){.l-main__sidebar{border-inline-end:1px solid var(--spruce-base-color-border);display:flex !important;inset-block-start:3rem;max-block-size:calc(100vh - 3rem * 2);position:sticky}}.l-main__sidebar--open{display:flex !important}.l-main__body{display:flex;flex-direction:column;inline-size:100%;margin-inline:auto;max-inline-size:var(--spruce-max-content-inline-size)}.l-card{--columns: 1;counter-reset:card;display:grid;gap:3rem;grid-template-columns:repeat(var(--columns), minmax(0, 1fr))}@media(min-width: 80em){.l-card{--columns: 2}}.l-front-page{display:grid;gap:4.5rem;grid-template-columns:minmax(0, 1fr)}.l-post{--gtc: minmax(0, 1fr);display:grid;gap:var(--spruce-container-gap);grid-template-columns:var(--gtc)}@media(min-width: 80em){.l-post{--gtc: minmax(0, 1fr) minmax(0, 15rem)}}@media(min-width: 80em){.l-post__toc{order:2}}.l-post__toc .toc{inset-block-start:3rem;position:sticky}@media(min-width: 80em){.l-post .post-heading{grid-column:1/3}}.l-list{--gtc: minmax(0, 1fr);display:grid;gap:3rem;grid-template-columns:var(--gtc)}.l-list__inner--changelog>*,.l-list__inner--faq>*{margin-block-end:0;margin-block-start:0}.l-list__inner--changelog>*+*,.l-list__inner--faq>*+*{margin-block-start:3rem}.header{align-items:center;border-block-end:1px solid var(--spruce-base-color-border);display:flex;flex-wrap:wrap;gap:1rem clamp(1.5rem, 5vw, 3rem);justify-content:space-between;order:2;padding-block:1.75rem}@media(min-width: 48em){.header{order:-1}}.header__column{align-items:center;display:flex;flex-grow:1;gap:clamp(1.5rem, 5vw, 3rem);justify-content:space-between}@media(min-width: 48em){.header__column{justify-content:flex-start}}.header__logo{display:inline-flex}.header__logo img,.header__logo svg{block-size:2rem;display:inline-flex;inline-size:auto}.header__toggle{--spruce-border-radius:1rem 1rem 0 1rem}@media(min-width: 48em){.header__toggle{display:none}}.header__actions{align-items:center;display:flex;flex-wrap:wrap;gap:clamp(1rem, 5vw, 1.5rem);margin-inline-start:auto}.header__navigation{display:none}@media(min-width: 48em){.header__navigation{display:flex}}.header__navigation ul{list-style:none;margin:0;padding:0;align-items:center;background-color:rgba(0,0,0,0);display:flex;flex-direction:row;flex-wrap:wrap;gap:.5rem 1.5rem;inset:auto;padding:0;position:relative}@media(min-width: 80em){.header__navigation ul{gap:.5rem 3rem}}.header__navigation li{margin-block:0}.header__navigation a{align-items:center;color:var(--spruce-base-color-heading);display:flex;gap:.5rem;text-decoration:none}.header__navigation a:hover{color:var(--spruce-base-color-primary)}.header__navigation a[aria-current=page]{font-weight:700}.header__navigation svg{--dimension: 0.65em;block-size:var(--dimension);color:var(--spruce-navigation-color-arrow);inline-size:var(--dimension)}.header__socials{align-items:center;display:flex;gap:1rem}.header__socials a{color:var(--spruce-base-color-heading);display:inline-flex}.header__socials a:hover,.header__socials a:focus{color:var(--spruce-base-color-primary)}.header__socials svg{--dimension: 1.25rem;block-size:var(--dimension);inline-size:var(--dimension)}.footer{--logo-block-size: 4rem;--logo-inline-size: 4rem;border-block-start:1px solid var(--spruce-base-color-border);margin-block-start:clamp(3rem, 5vw, 4.5rem);padding-block-start:clamp(3rem, 5vw, 4.5rem)}.footer__inner{display:grid;gap:3rem}@supports(inline-size: min(12rem, 100%)){.footer__inner{grid-template-columns:repeat(auto-fit, minmax(min(12rem, 100%), 1fr))}}.footer__logo{block-size:var(--logo-block-size);display:flex;inline-size:var(--logo-inline-size)}.footer__title{font-size:clamp(1.0625rem, 2vw + 1rem, 1.25rem)}.footer__navigation{list-style:none;margin:0;padding:0}.footer__navigation>*{margin-block-end:0;margin-block-start:0}.footer__navigation>*+*{margin-block-start:.5rem}.footer__navigation a{align-items:center;color:var(--spruce-base-color-text);display:inline-flex;gap:.5rem;text-decoration:none}.footer__navigation a:hover,.footer__navigation a:focus,.footer__navigation a:active,.footer__navigation a[aria-current=page]{color:var(--spruce-base-color-primary)}.footer__navigation a[aria-current=page]{font-weight:700}.footer__navigation a svg{--dimension: 0.65em;block-size:var(--dimension);color:var(--spruce-navigation-color-arrow);inline-size:var(--dimension)}.footer__column>*{margin-block-end:0;margin-block-start:0}.footer__column>*+*{margin-block-start:1.5rem}.skip-link{inset:-50vh auto auto 1.5rem;position:fixed;z-index:100}.skip-link:focus{inset-block-start:1.5rem}.breadcrumb-list{list-style:none;margin:0;padding:0;align-items:center;display:flex;max-inline-size:100%;overflow-x:auto;white-space:nowrap}.breadcrumb-list>li{align-items:center;display:inline-flex;margin-block:0}.breadcrumb-list>li+li::before{block-size:.4em;border-block-end:2px solid var(--spruce-breadcrumb-color-separator);border-inline-end:2px solid var(--spruce-breadcrumb-color-separator);content:"";display:inline-flex;inline-size:.4em;margin-inline:.75em;transform:rotate(-45deg)}[dir=rtl] .breadcrumb-list>li+li::before{transform:rotate(45deg)}.breadcrumb-list a{text-decoration:none}.breadcrumb-list [aria-current=page]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;max-inline-size:20ch;text-align:start}.open-search{position:relative;align-items:center;display:flex;gap:.5rem}.open-search__btn::before{content:"";inset:0;position:absolute}.open-search__icon{--dimension: 1rem;block-size:var(--dimension);color:var(--spruce-search-color-icon);inline-size:var(--dimension)}.no-transition *{transition:none !important}.theme-switcher{color:var(--spruce-base-color-text);display:inline-flex;position:relative}.theme-switcher[data-theme-mode=system] .theme-switcher__system-mode{display:flex}.theme-switcher[data-theme-mode=light] .theme-switcher__light-mode{display:flex}.theme-switcher[data-theme-mode=dark] .theme-switcher__dark-mode{display:flex}.theme-switcher button{display:none}.theme-switcher button>*{pointer-events:none}.post-heading>*{margin-block-end:0;margin-block-start:0}.post-heading>*+*{margin-block-start:1rem}.post-heading__headline{color:var(--spruce-base-color-primary);font-family:var(--spruce-font-family-heading);font-weight:700;letter-spacing:.1em;text-transform:uppercase}.post-heading__title{font-size:clamp(2.45rem, 4.5vw, 3.5rem);line-height:var(--spruce-line-height-sm);margin-block-start:.5rem;max-inline-size:20ch}.post-heading__description{font-size:var(--spruce-font-size-lead);max-inline-size:60ch}.post-heading__actions{display:flex;flex-wrap:wrap;gap:1rem;margin-block-start:1.5rem}.post-content>*{margin-block-end:0;margin-block-start:0}.post-content>*+*{margin-block-start:1.5rem}@media(min-width: 64em){.post-content{font-size:1.0375rem}}.post-content :is(dd,dl,dl,h1,h2,h3:not(.accordion-card__title),h4,h5,h6,hr,ul,ol,p,blockquote){max-inline-size:40rem}.post-content *+h2,.post-content *+h3{margin-top:3rem}.post-content h2+*,.post-content h3+*,.post-content h4+*,.post-content h5+*,.post-content h6+*{margin-top:1rem}.post-content h2[id]{align-items:flex-start;display:flex;justify-content:space-between}.post-content h2[id]:hover .anchor,.post-content h2[id]:focus-within .anchor{opacity:1}.post-content h2[id] .anchor{font-weight:600;opacity:0;text-decoration:none}.post-content img,.post-content iframe{border-radius:var(--spruce-border-radius-sm)}.post-content iframe{aspect-ratio:16/9}.post-content a>code{color:var(--spruce-base-color-link)}.post-content strong{color:var(--spruce-base-color-heading)}.post-content picture{display:flex}.toc>*{margin-block-end:0;margin-block-start:0}.toc>*+*{margin-block-start:1rem}.toc__title{font-size:clamp(1.0625rem, 2vw + 1rem, 1.25rem)}.toc__navigation ol{list-style:none;margin:0;padding:0}.toc__navigation ol>*{margin-block-end:0;margin-block-start:0}.toc__navigation ol>*+*{margin-block-start:.5rem}.toc__navigation a{color:var(--spruce-base-color-text);text-decoration:none}.toc__navigation a:hover,.toc__navigation a:focus{color:var(--spruce-base-color-primary)}.sidebar{display:flex;flex-direction:column;gap:1.5rem}@media(min-width: 48em){.sidebar{padding-inline-end:var(--spruce-container-gap)}}.sidebar__footer{align-items:center;display:flex;justify-content:space-between}.sidebar__body{display:flex;flex-direction:column;flex-grow:1;gap:1.5rem;max-block-size:calc(100vh - var(--dimension) - 1.5rem * 3);overflow-y:auto}.sidebar__body::-webkit-scrollbar{block-size:.5rem;inline-size:.5rem}.sidebar__body::-webkit-scrollbar-thumb{background:var(--spruce-scrollbar-color-thumb-background);border-radius:.05rem}.sidebar__body::-webkit-scrollbar-thumb:hover{background:var(--spruce-scrollbar-color-thumb-background-hover)}.sidebar__body::-webkit-scrollbar-track{background:var(--spruce-scrollbar-color-track-background);border-radius:.05rem}@media(min-width: 48em){.sidebar__body{max-block-size:initial}}@media(min-width: 48em){.sidebar-section--navigation{display:none}}.sidebar-section__title{color:var(--spruce-base-color-heading);font-size:var(--spruce-font-size-base);margin-block:0}.sidebar-section__navigation{border-inline-start:1px solid var(--spruce-base-color-border);font-size:1rem;margin-block-start:1rem;padding-inline-start:1rem}.sidebar-section__navigation ul{list-style:none;margin:0;padding:0}.sidebar-section__navigation a{align-items:center;color:var(--spruce-base-color-text);display:inline-flex;gap:.5rem;text-decoration:none}.sidebar-section__navigation a[aria-current=page]{color:var(--spruce-base-color-heading);font-weight:700;position:relative}.sidebar-section__navigation a[aria-current=page]::before{background-color:var(--spruce-base-color-primary);border-end-end-radius:var(--spruce-border-radius-sm);border-start-end-radius:var(--spruce-border-radius-sm);content:"";inline-size:.3rem;inset-block:0;inset-inline-start:-1rem;position:absolute}.sidebar-section__navigation a svg{--dimension: 0.65em;block-size:var(--dimension);color:var(--spruce-navigation-color-arrow);inline-size:var(--dimension)}.post-card{position:relative;background-color:var(--spruce-card-color-background);border:1px solid var(--spruce-base-color-border);border-radius:var(--spruce-border-radius-sm);padding:3rem clamp(1.5rem, 5vw, 3rem)}.post-card>*{margin-block-end:0;margin-block-start:0}.post-card>*+*{margin-block-start:1rem}.card__link::before{content:"";inset:0;position:absolute}.post-card__serial-number{color:var(--spruce-base-color-primary);font-family:var(--spruce-font-family-heading);font-size:clamp(2.8rem, 4vw + 1rem, 4rem);font-weight:800;line-height:.8}.post-card__serial-number::before{content:counter(card, decimal-leading-zero);counter-increment:card}.post-card__title{font-size:clamp(1.328125rem, 2vw + 1rem, 1.5625rem)}code[class*=language-],pre[class*=language-]{border-radius:var(--spruce-border-radius-sm);color:var(--spruce-prism-color-color);font-family:var(--spruce-font-family-cursive);font-size:var(--spruce-font-size-base);hyphens:none;line-height:1.5;tab-size:4;text-align:left;white-space:pre;word-break:normal;word-spacing:normal;word-wrap:normal}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{display:grid;overflow:auto;padding:1.5rem}pre[class*=language-] code{background-color:rgba(0,0,0,0);padding:0}:not(pre)>code[class*=language-],pre[class*=language-]{background:var(--spruce-prism-color-background);overflow-x:auto}.token.comment,.token.prolog,.token.cdata{color:var(--spruce-prism-color-comment);font-style:italic}.token.punctuation{color:var(--spruce-prism-color-punctuation)}.namespace{color:var(--spruce-prism-color-namespace)}.token.deleted{color:var(--spruce-prism-color-deleted);font-style:italic}.token.symbol,.token.operator,.token.keyword,.token.property{color:var(--spruce-prism-color-namespace)}.token.tag{color:var(--spruce-prism-color-punctuation)}.token.boolean{color:var(--spruce-prism-color-boolean)}.token.number{color:var(--spruce-prism-color-number)}.token.constant,.token.builtin,.token.string,.token.url,.token.entity,.language-css .token.string,.style .token.string,.token.char{color:var(--spruce-prism-color-constant)}.token.selector,.token.function,.token.doctype{color:var(--spruce-prism-color-punctuation);font-style:italic}.token.attr-name,.token.inserted{color:var(--spruce-prism-color-constant);font-style:italic}.token.class-name,.token.atrule{color:var(--spruce-prism-color-class-name)}.token.regex,.token.important,.token.variable{color:var(--spruce-prism-color-regex)}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.modal-backdrop{backdrop-filter:blur(0.35rem);background-color:rgba(0,0,0,.35);display:none;inset:0;padding:1rem;position:fixed}.modal-backdrop--open{display:block}.modal{background-color:var(--spruce-base-color-background);border-radius:var(--spruce-border-radius-sm);box-shadow:var(--spruce-box-shadow);margin-block:7rem;margin-inline:auto;max-inline-size:40rem;padding:clamp(1rem, 5vw, 1.5rem)}.pagefind-ui__search-input{--webkit-date-line-height: 1.375;--spruce-border-radius: 0.425rem;--spruce-border-width: 1px;--spruce-font-size: 1rem;--spruce-line-height: 1.5;--spruce-padding: 0.5em 0.75em;--spruce-textarea-block-size: 6rem;appearance:none;background-color:var(--spruce-form-color-background);border:var(--spruce-border-width) solid var(--spruce-form-color-border);border-radius:var(--spruce-border-radius);box-sizing:border-box;color:var(--spruce-form-color-text);display:block;font-size:var(--spruce-font-size);inline-size:100%;line-height:var(--spruce-line-height);padding:var(--spruce-padding);transition-duration:var(--spruce-duration);transition-property:border,box-shadow;transition-timing-function:var(--spruce-timing-function)}.pagefind-ui__search-input::placeholder{color:var(--spruce-form-color-placeholder)}.pagefind-ui__search-input::-webkit-datetime-edit{line-height:var(--webkit-date-line-height)}.pagefind-ui__search-input:focus{border-color:var(--spruce-form-color-border-focus);box-shadow:0 0 0 .25rem var(--spruce-form-color-ring-focus);outline:2px solid rgba(0,0,0,0)}.pagefind-ui__search-input[type=color]{--spruce-aspect-ratio: 1;--spruce-block-size: 100%;--spruce-inline-size: 2.625rem;--spruce-padding: 0.5em;aspect-ratio:var(--spruce-aspect-ratio);block-size:var(--spruce-block-size);inline-size:var(--spruce-inline-size);padding:var(--spruce-padding)}.pagefind-ui__search-input[type=color]::-webkit-color-swatch-wrapper{padding:0}.pagefind-ui__search-input[type=color]::-moz-color-swatch{border:0;border-radius:var(--spruce-border-radius)}.pagefind-ui__search-input[type=color]::-webkit-color-swatch{border:0;border-radius:var(--spruce-border-radius)}.pagefind-ui__search-input[disabled],.pagefind-ui__search-input[disabled=true]{background-color:var(--spruce-form-color-background-disabled);border-color:var(--spruce-form-color-border-disabled);cursor:not-allowed}textarea.pagefind-ui__search-input{block-size:var(--spruce-textarea-block-size);min-block-size:var(--spruce-textarea-block-size);resize:vertical}.pagefind-ui{position:relative}.pagefind-ui__search-input{padding:.5em .75em !important}.pagefind-ui__form>*{margin-block-end:0;margin-block-start:0}.pagefind-ui__form>*+*{margin-block-start:1rem}.pagefind-ui__search-clear{background:none;border:0;font-size:var(--spruce-font-size-sm);inset-block-start:.8em;inset-inline-end:.5em;margin-block-start:0;position:absolute;text-transform:uppercase}.pagefind-ui__drawer{max-block-size:20rem;overflow-y:auto}.pagefind-ui__drawer::-webkit-scrollbar{block-size:.5rem;inline-size:.5rem}.pagefind-ui__drawer::-webkit-scrollbar-thumb{background:var(--spruce-scrollbar-color-thumb-background);border-radius:var(--spruce-border-radius-sm)}.pagefind-ui__drawer::-webkit-scrollbar-thumb:hover{background:var(--spruce-scrollbar-color-thumb-background-hover)}.pagefind-ui__drawer::-webkit-scrollbar-track{background:var(--spruce-scrollbar-color-track-background);border-radius:var(--spruce-border-radius-sm)}.pagefind-ui__results{list-style:none;margin:0;padding:0;padding-inline-end:1.5rem}.pagefind-ui__results>*{margin-block-end:0;margin-block-start:0}.pagefind-ui__results>*+*{margin-block-start:1rem}.pagefind-ui__results:empty{display:none}.pagefind-ui__results-area>*{margin-block-end:0;margin-block-start:0}.pagefind-ui__results-area>*+*{margin-block-start:.5rem}.pagefind-ui__result-inner>*{margin-block-end:0;margin-block-start:0}.pagefind-ui__result-inner>*+*{margin-block-start:.25rem}.pagefind-ui__result-title{font-weight:700}.pagefind-ui__hidden{display:none}.accordion-list>*{margin-block-end:0;margin-block-start:0}.accordion-list>*+*{margin-block-start:1rem}.accordion-card{background-color:var(--spruce-base-color-background);border:1px solid var(--spruce-base-color-border);border-radius:var(--spruce-border-radius-sm);max-inline-size:75ch}.accordion-card--js .accordion-card__title{padding:0}.accordion-card__title{font-family:var(--spruce-font-family-base);font-size:clamp(1.0625rem, 2vw + 1rem, 1.25rem);margin-block:0;padding:1.5rem}.accordion-card__toggle{background:none;border:0;color:inherit;cursor:pointer;font:inherit;outline:inherit;padding:0;align-items:center;display:flex;gap:1.5rem;inline-size:100%;justify-content:space-between;padding:1.5rem;text-align:start}.accordion-card__toggle:focus-visible svg{outline:2px solid var(--spruce-base-color-primary);outline-offset:2px}.accordion-card__toggle svg{--dimension: 1.75rem;background-color:var(--spruce-btn-color-primary-background);block-size:var(--dimension);border-radius:var(--spruce-border-radius-sm);color:var(--spruce-btn-color-primary-foreground);flex-shrink:0;inline-size:var(--dimension)}.accordion-card__toggle[aria-expanded=true] .vertical-line{display:none}.accordion-card__content{padding-block-end:1.5rem;padding-inline:1.5rem}.accordion-card__content>*{margin-block-end:0;margin-block-start:0}.accordion-card__content>*+*{margin-block-start:.5rem}.changelog-item{--gtc: minmax(0, 1fr);align-items:flex-start;display:grid;gap:1.5rem 3rem;grid-template-columns:var(--gtc)}@media(min-width: 64em){.changelog-item{--gtc: minmax(0, 9rem) minmax(0, 1fr)}}.changelog-item__date{background-color:var(--spruce-btn-color-primary-background);border-radius:var(--spruce-border-radius-sm);color:var(--spruce-btn-color-primary-foreground);display:inline-flex;flex-shrink:0;font-weight:700;justify-self:start;padding:.25rem 1rem}.changelog-item ul p{margin-block:0}.group>*{margin-block-end:0;margin-block-start:0}.group>*+*{margin-block-start:1.5rem}.post-navigation{align-items:center;border-block-start:1px solid var(--spruce-base-color-border);display:flex;flex-wrap:wrap;gap:1.5rem;justify-content:space-between;margin-block-start:4.5rem;padding-block:1.5rem}.post-navigation-item{align-items:center;display:flex;gap:1rem;text-decoration:none}.post-navigation-item:hover .post-navigation-item__icon{background-color:var(--spruce-navigation-color-icon-background-hover);color:var(--spruce-navigation-color-icon-foreground-hover)}.post-navigation-item--next{margin-inline-start:auto;text-align:end}.post-navigation-item__icon{transition-duration:var(--spruce-duration);transition-property:all;transition-timing-function:var(--spruce-timing-function);align-items:center;background-color:var(--spruce-navigation-color-icon-background);block-size:3rem;border-radius:.425rem;color:var(--spruce-navigation-color-icon-foreground);display:flex;flex-shrink:0;inline-size:3rem;justify-content:center}.post-navigation-item__icon svg{--dimension: 1rem;block-size:var(--dimension);inline-size:var(--dimension)}[dir=rtl] .post-navigation-item__icon svg{transform:rotate(180deg)}.post-navigation-item__caption{color:var(--spruce-base-color-text);line-height:var(--spruce-line-height-md)}.post-navigation-item__title{color:var(--spruce-base-color-primary);display:flex;font-weight:700}:root{--spruce-section-gap:clamp(5rem, 7vw, 7rem);--spruce-container-gap:clamp(1.5rem, 5vw, 3rem);--spruce-max-content-inline-size:70rem;--spruce-box-shadow:0 0.75rem 1.25rem hsla(0, 0%, 0%, 0.05)} 2 | --------------------------------------------------------------------------------