├── .browserslistrc ├── .editorconfig ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── assets ├── images │ └── .gitkeep ├── scripts │ ├── app.js │ ├── config.js │ ├── globals.js │ ├── modules.js │ ├── modules │ │ ├── Example.js │ │ ├── Load.js │ │ └── Scroll.js │ ├── utils │ │ ├── dom.js │ │ ├── fonts.js │ │ ├── grid-helper.js │ │ ├── html.js │ │ ├── image.js │ │ ├── is.js │ │ ├── maths.js │ │ ├── tickers.js │ │ ├── transform.js │ │ └── visibility.js │ └── vendors │ │ └── .gitkeep ├── styles │ ├── components │ │ ├── _button.scss │ │ ├── _form.scss │ │ ├── _heading.scss │ │ ├── _scrollbar.scss │ │ └── _text.scss │ ├── critical.scss │ ├── elements │ │ ├── _document.scss │ │ └── _normalize.scss │ ├── main.scss │ ├── objects │ │ ├── _container.scss │ │ ├── _grid.scss │ │ ├── _icons.scss │ │ ├── _layout.scss │ │ ├── _ratio.scss │ │ └── _table.scss │ ├── settings │ │ ├── _config.breakpoints.scss │ │ ├── _config.colors.scss │ │ ├── _config.eases.scss │ │ ├── _config.fonts.scss │ │ ├── _config.scss │ │ ├── _config.spacings.scss │ │ ├── _config.speeds.scss │ │ ├── _config.variables.scss │ │ └── _config.zindexes.scss │ ├── tools │ │ ├── _family.scss │ │ ├── _functions.scss │ │ ├── _layout.scss │ │ ├── _maths.scss │ │ ├── _mixins.scss │ │ └── _widths.scss │ ├── utilities │ │ ├── _align.scss │ │ ├── _grid-column.scss │ │ ├── _helpers.scss │ │ ├── _print.scss │ │ ├── _ratio.scss │ │ ├── _spacing.scss │ │ ├── _states.scss │ │ └── _widths.scss │ └── vendors │ │ └── .gitkeep └── svgs │ └── .gitkeep ├── build ├── build.js ├── helpers │ ├── config.js │ ├── glob.js │ ├── message.js │ ├── notification.js │ ├── postcss.js │ └── template.js ├── tasks │ ├── concats.js │ ├── scripts.js │ ├── styles.js │ ├── svgs.js │ └── versions.js ├── utils │ └── index.js └── watch.js ├── docs ├── development.md ├── grid.md └── technologies.md ├── loconfig.example.json ├── loconfig.json ├── package-lock.json ├── package.json └── www ├── assets ├── emails │ └── index.html ├── fonts │ ├── .gitkeep │ ├── SourceSans3-Bold.woff2 │ ├── SourceSans3-BoldIt.woff2 │ ├── SourceSans3-Regular.woff2 │ └── SourceSans3-RegularIt.woff2 ├── images │ ├── favicons │ │ ├── android-chrome-144x144.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ └── sprite.svg ├── scripts │ └── .gitkeep └── styles │ └── .gitkeep ├── browserconfig.xml ├── favicon.ico ├── form.html ├── grid.html ├── images.html ├── index.html └── site.webmanifest /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{md,markdown}] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{ms,mustache}] 16 | insert_final_newline = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | Thumbs.db 4 | loconfig.*.json 5 | !loconfig.example.json 6 | .prettierrc 7 | 8 | www/assets/scripts/* 9 | !www/assets/scripts/.gitkeep 10 | www/assets/styles/* 11 | !www/assets/styles/.gitkeep 12 | 13 | assets.json 14 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Locomotive, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This repository is no longer maintained. We recommend checking out our [Astro](https://github.com/locomotivemtl/astro-boilerplate) or [Craft](https://github.com/locomotivemtl/craft-boilerplate) boilerplates instead. 3 | 4 |

5 | 6 | 7 | 8 |

9 |

Locomotive Boilerplate

10 |

Front-end boilerplate for projects by Locomotive.

11 | 12 | ## Features 13 | 14 | * Uses a custom [task runner](docs/development.md) for handling assets. 15 | * Uses [BrowserSync] for fast development and testing in browsers. 16 | * Uses [Sass] for a feature rich superset of CSS. 17 | * Uses [ESBuild] for extremely fast processing of JS/ES modules. 18 | * Uses [SVG Mixer] for processing SVG files and generating spritesheets. 19 | * Uses [ITCSS] for a sane and scalable CSS architecture. 20 | * Uses [Locomotive Scroll] for smooth scrolling with parallax effects. 21 | * Uses a custom [grid system](docs/grid.md) for layout creation. 22 | 23 | Learn more about [languages and technologies](docs/technologies.md). 24 | 25 | ## Getting started 26 | 27 | Make sure you have the following installed: 28 | 29 | * [Node] — at least 20, the latest LTS is recommended. 30 | * [NPM] — at least 10, the latest LTS is recommended. 31 | 32 | > 💡 You can use [NVM] to install and use different versions of Node via the command-line. 33 | 34 | ```sh 35 | # Clone the repository. 36 | git clone https://github.com/locomotivemtl/locomotive-boilerplate.git my-new-project 37 | 38 | # Enter the newly-cloned directory. 39 | cd my-new-project 40 | ``` 41 | 42 | Then replace the original remote repository with your project's repository. 43 | 44 | Then update the following files to suit your project: 45 | 46 | * [`README.md`](README.md): 47 | The file you are currently reading. 48 | * [`package.json`](package.json): 49 | * Package name: `@locomotivemtl/boilerplate` 50 | * Package title: `Locomotive Boilerplate` 51 | * [`package-lock.json`](package-lock.json): 52 | * Package name: `@locomotivemtl/boilerplate` 53 | * [`loconfig.json`](loconfig.json): 54 | * BrowserSync proxy URL: `locomotive-boilerplate.test` 55 | Remove `paths.url` to use BrowserSync's built-in server which uses `paths.dest`. 56 | * View path: `./views/boilerplate/template` 57 | * [`environment.js`](assets/scripts/utils/environment.js): 58 | * Application name: `Boilerplate` 59 | * [`site.webmanifest`](www/site.webmanifest): 60 | * Manifest name: `Locomotive Boilerplate` 61 | * Manifest short name: `Boilerplate` 62 | * HTML files: 63 | * Page title: `Locomotive Boilerplate` 64 | 65 | ## Installation 66 | 67 | ```sh 68 | # Switch to recommended Node version from .nvmrc 69 | nvm use 70 | 71 | # Install dependencies from package.json 72 | npm install 73 | ``` 74 | 75 | ## Development 76 | 77 | ```sh 78 | # Start development server, watch for changes, and compile assets 79 | npm start 80 | 81 | # Compile and minify assets 82 | npm run build 83 | ``` 84 | 85 | Learn more about [development and building](docs/development.md). 86 | 87 | ## Documentation 88 | 89 | * [Development and building](docs/development.md) 90 | * [Languages and technologies](docs/technologies.md) 91 | * [Grid system](docs/grid.md) 92 | 93 | [BrowserSync]: https://npmjs.com/package/browser-sync 94 | [ESBuild]: https://npmjs.com/package/esbuild 95 | [ITCSS]: https://itcss.io/ 96 | [Locomotive Scroll]: https://npmjs.com/package/locomotive-scroll 97 | [modularJS]: https://npmjs.com/package/modujs 98 | [modularLoad]: https://npmjs.com/package/modularload 99 | [Sass]: https://sass-lang.com/ 100 | [SVG Mixer]: https://npmjs.com/package/svg-mixer 101 | [Node]: https://nodejs.org/ 102 | [NPM]: https://npmjs.com/ 103 | [NVM]: https://github.com/nvm-sh/nvm 104 | -------------------------------------------------------------------------------- /assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/assets/images/.gitkeep -------------------------------------------------------------------------------- /assets/scripts/app.js: -------------------------------------------------------------------------------- 1 | import modular from 'modujs'; 2 | import * as modules from './modules'; 3 | import globals from './globals'; 4 | import { debounce } from './utils/tickers'; 5 | import { $html } from './utils/dom'; 6 | import { ENV, FONT, CUSTOM_EVENT, CSS_CLASS } from './config' 7 | import { isFontLoadingAPIAvailable, loadFonts } from './utils/fonts'; 8 | 9 | const app = new modular({ 10 | modules, 11 | }); 12 | 13 | function init() { 14 | bindEvents(); 15 | globals(); 16 | setViewportSizes(); 17 | 18 | app.init(app); 19 | 20 | $html.classList.add(CSS_CLASS.LOADED, CSS_CLASS.READY); 21 | $html.classList.remove(CSS_CLASS.LOADING); 22 | 23 | /** 24 | * Debug focus 25 | */ 26 | // document.addEventListener( 27 | // "focusin", 28 | // function () { 29 | // console.log('focused: ', document.activeElement) 30 | // }, true 31 | // ); 32 | 33 | /** 34 | * Eagerly load the following fonts. 35 | */ 36 | if (isFontLoadingAPIAvailable) { 37 | loadFonts(FONT.EAGER, ENV.IS_DEV).then((eagerFonts) => { 38 | $html.classList.add(CSS_CLASS.FONTS_LOADED); 39 | 40 | /** 41 | * Debug fonts loading 42 | */ 43 | // if (ENV.IS_DEV) { 44 | // console.group('Eager fonts loaded!', eagerFonts.length, '/', document.fonts.size); 45 | // console.group('State of eager fonts:'); 46 | // eagerFonts.forEach(font => console.log(font.family, font.style, font.weight, font.status)); 47 | // console.groupEnd(); 48 | // console.group('State of all fonts:'); 49 | // document.fonts.forEach(font => console.log(font.family, font.style, font.weight, font.status)); 50 | // console.groupEnd(); 51 | // } 52 | }); 53 | } 54 | } 55 | 56 | //////////////// 57 | // Global events 58 | //////////////// 59 | function bindEvents() { 60 | 61 | // Resize event 62 | const resizeEndEvent = new CustomEvent(CUSTOM_EVENT.RESIZE_END) 63 | window.addEventListener( 64 | "resize", 65 | debounce(() => { 66 | window.dispatchEvent(resizeEndEvent) 67 | }, 200, false) 68 | ) 69 | window.addEventListener( 70 | "resize", 71 | onResize 72 | ) 73 | } 74 | 75 | function onResize() { 76 | setViewportSizes() 77 | } 78 | 79 | function setViewportSizes() { 80 | 81 | // Document styles 82 | const documentStyles = document.documentElement.style; 83 | 84 | // Viewport width 85 | const vw = document.body.clientWidth * 0.01; 86 | documentStyles.setProperty('--vw', `${vw}px`); 87 | 88 | // Return if browser supports vh, svh, dvh, & lvh 89 | if (ENV.SUPPORTS_VH) { 90 | return 91 | } 92 | 93 | // Viewport height 94 | const svh = document.documentElement.clientHeight * 0.01; 95 | documentStyles.setProperty('--svh', `${svh}px`); 96 | 97 | const dvh = window.innerHeight * 0.01; 98 | documentStyles.setProperty('--dvh', `${dvh}px`); 99 | 100 | if (document.body) { 101 | const fixed = document.createElement('div'); 102 | fixed.style.width = '1px'; 103 | fixed.style.height = '100vh'; 104 | fixed.style.position = 'fixed'; 105 | fixed.style.left = '0'; 106 | fixed.style.top = '0'; 107 | fixed.style.bottom = '0'; 108 | fixed.style.visibility = 'hidden'; 109 | 110 | document.body.appendChild(fixed); 111 | 112 | var fixedHeight = fixed.clientHeight; 113 | 114 | fixed.remove(); 115 | 116 | const lvh = fixedHeight * 0.01; 117 | 118 | documentStyles.setProperty('--lvh', `${lvh}px`); 119 | } 120 | } 121 | 122 | //////////////// 123 | // Execute 124 | //////////////// 125 | window.addEventListener('load', () => { 126 | const $style = document.getElementById('main-css'); 127 | 128 | if ($style) { 129 | if ($style.isLoaded) { 130 | init(); 131 | } else { 132 | $style.addEventListener('load', init); 133 | } 134 | } else { 135 | console.warn('The "main-css" stylesheet not found'); 136 | } 137 | }); 138 | -------------------------------------------------------------------------------- /assets/scripts/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * > When using the esBuild API, all `process.env.NODE_ENV` expressions 3 | * > are automatically defined to `"production"` if all minification 4 | * > options are enabled and `"development"` otherwise. This only happens 5 | * > if `process`, `process.env`, and `process.env.NODE_ENV` are not already 6 | * > defined. This substitution is necessary to avoid code crashing instantly 7 | * > (since `process` is a Node API, not a web API). 8 | * > — https://esbuild.github.io/api/#platform 9 | */ 10 | 11 | const NODE_ENV = process.env.NODE_ENV 12 | const IS_MOBILE = window.matchMedia('(any-pointer:coarse)').matches 13 | 14 | // Main environment variables 15 | const ENV = Object.freeze({ 16 | // Node environment 17 | NAME: NODE_ENV, 18 | IS_PROD: NODE_ENV === 'production', 19 | IS_DEV: NODE_ENV === 'development', 20 | 21 | // Device 22 | IS_MOBILE, 23 | IS_DESKTOP: !IS_MOBILE, 24 | 25 | // Supports 26 | SUPPORTS_VH: ( 27 | 'CSS' in window 28 | && 'supports' in window.CSS 29 | && window.CSS.supports('height: 100svh') 30 | && window.CSS.supports('height: 100dvh') 31 | && window.CSS.supports('height: 100lvh') 32 | ) 33 | }) 34 | 35 | // Main CSS classes used within the project 36 | const CSS_CLASS = Object.freeze({ 37 | LOADING: 'is-loading', 38 | LOADED: 'is-loaded', 39 | READY: 'is-ready', 40 | FONTS_LOADED: 'fonts-loaded', 41 | LAZY_CONTAINER: 'c-lazy', 42 | LAZY_LOADED: '-lazy-loaded', 43 | // ... 44 | }) 45 | 46 | // Custom js events 47 | const CUSTOM_EVENT = Object.freeze({ 48 | RESIZE_END: 'loco.resizeEnd', 49 | // ... 50 | }) 51 | 52 | // Fonts parameters 53 | const FONT = Object.freeze({ 54 | EAGER: [ 55 | { family: 'Source Sans', style: 'normal', weight: 400 }, 56 | { family: 'Source Sans', style: 'normal', weight: 700 }, 57 | ], 58 | }) 59 | 60 | export { 61 | ENV, 62 | CSS_CLASS, 63 | CUSTOM_EVENT, 64 | FONT, 65 | } 66 | -------------------------------------------------------------------------------- /assets/scripts/globals.js: -------------------------------------------------------------------------------- 1 | import { ENV } from './config'; 2 | 3 | // Dynamic imports for development mode only 4 | let gridHelper; 5 | (async () => { 6 | if (ENV.IS_DEV) { 7 | const gridHelperModule = await import('./utils/grid-helper'); 8 | gridHelper = gridHelperModule?.gridHelper; 9 | } 10 | })(); 11 | 12 | export default function () { 13 | /** 14 | * Add grid helper 15 | */ 16 | gridHelper?.(); 17 | } 18 | -------------------------------------------------------------------------------- /assets/scripts/modules.js: -------------------------------------------------------------------------------- 1 | export {default as Example} from './modules/Example'; 2 | export {default as Load} from './modules/Load'; 3 | export {default as Scroll} from './modules/Scroll'; 4 | -------------------------------------------------------------------------------- /assets/scripts/modules/Example.js: -------------------------------------------------------------------------------- 1 | import { module } from 'modujs'; 2 | import { FONT } from '../config'; 3 | import { whenReady } from '../utils/fonts'; 4 | 5 | export default class extends module { 6 | constructor(m) { 7 | super(m); 8 | } 9 | 10 | init() { 11 | whenReady(FONT.EAGER).then((fonts) => this.onFontsLoaded(fonts)); 12 | } 13 | 14 | onFontsLoaded(fonts) { 15 | console.log('Example: Eager Fonts Loaded!', fonts) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/scripts/modules/Load.js: -------------------------------------------------------------------------------- 1 | import { module } from 'modujs'; 2 | import modularLoad from 'modularload'; 3 | 4 | export default class extends module { 5 | constructor(m) { 6 | super(m); 7 | } 8 | 9 | init() { 10 | const load = new modularLoad({ 11 | enterDelay: 0, 12 | transitions: { 13 | customTransition: {} 14 | } 15 | }); 16 | 17 | load.on('loaded', (transition, oldContainer, newContainer) => { 18 | this.call('destroy', oldContainer, 'app'); 19 | this.call('update', newContainer, 'app'); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/scripts/modules/Scroll.js: -------------------------------------------------------------------------------- 1 | import { module } from 'modujs' 2 | import { lazyLoadImage } from '../utils/image' 3 | import LocomotiveScroll from 'locomotive-scroll' 4 | 5 | export default class extends module { 6 | constructor(m) { 7 | super(m); 8 | } 9 | 10 | init() { 11 | this.scroll = new LocomotiveScroll({ 12 | modularInstance: this, 13 | }) 14 | 15 | // // Force scroll to top 16 | // if (history.scrollRestoration) { 17 | // history.scrollRestoration = 'manual' 18 | // window.scrollTo(0, 0) 19 | // } 20 | } 21 | 22 | /** 23 | * Lazy load the related image. 24 | * 25 | * @see ../utils/image.js 26 | * 27 | * It is recommended to wrap your `` into an element with the 28 | * CSS class name `.c-lazy`. The CSS class name modifier `.-lazy-loaded` 29 | * will be applied on both the image and the parent wrapper. 30 | * 31 | * ```html 32 | *
33 | * 34 | *
35 | * ``` 36 | * 37 | * @param {LocomotiveScroll} args - The Locomotive Scroll instance. 38 | */ 39 | lazyLoad(args) { 40 | lazyLoadImage(args.target, null, () => { 41 | //callback 42 | }) 43 | } 44 | 45 | scrollTo(params) { 46 | let { target, ...options } = params 47 | 48 | options = Object.assign({ 49 | // Defaults 50 | duration: 1, 51 | }, options) 52 | 53 | this.scroll?.scrollTo(target, options) 54 | } 55 | 56 | destroy() { 57 | this.scroll.destroy(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /assets/scripts/utils/dom.js: -------------------------------------------------------------------------------- 1 | const $html = document.documentElement 2 | const $body = document.body 3 | 4 | export { 5 | $html, 6 | $body, 7 | } 8 | -------------------------------------------------------------------------------- /assets/scripts/utils/grid-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid Helper 3 | * 4 | * Provides a grid based on the design guidelines and is helpful for web integration. 5 | * 6 | * - `Control + g` to toggle the grid 7 | * 8 | */ 9 | 10 | /** 11 | * @typedef {Object} GridHelperReference 12 | * 13 | * @property {string} [gutterCssVar=GRID_HELPER_GUTTER_CSS_VAR] - CSS variable used to define grid gutters. 14 | * @property {string} [marginCssVar=GRID_HELPER_MARGIN_CSS_VAR] - CSS variable used to define grid margins. 15 | * @property {string} [rgbaColor=GRID_HELPER_RGBA_COLOR] - RGBA color for the grid appearence. 16 | */ 17 | 18 | const GRID_HELPER_GUTTER_CSS_VAR = '--grid-gutter'; 19 | const GRID_HELPER_MARGIN_CSS_VAR = '--grid-margin'; 20 | const GRID_HELPER_RGBA_COLOR = 'rgba(255, 0, 0, .1)'; 21 | 22 | /** 23 | * Create a grid helper 24 | * 25 | * @param {GridHelperReference} 26 | * 27 | */ 28 | function gridHelper({ 29 | gutterCssVar = GRID_HELPER_GUTTER_CSS_VAR, 30 | marginCssVar = GRID_HELPER_MARGIN_CSS_VAR, 31 | rgbaColor = GRID_HELPER_RGBA_COLOR, 32 | } = {}) { 33 | // Set grid container 34 | const $gridContainer = document.createElement('div'); 35 | document.body.append($gridContainer); 36 | 37 | // Set grid appearence 38 | setGridHelperColumns($gridContainer, rgbaColor); 39 | setGridHelperStyles($gridContainer, gutterCssVar, marginCssVar); 40 | 41 | // Set grid interactivity 42 | setGridEvents($gridContainer, rgbaColor); 43 | } 44 | 45 | /** 46 | * Set grid container styles 47 | * 48 | * @param {HTMLElement} $container - DOM Element that contains a list of generated columns 49 | * @param {string} gutterCssVar - CSS variable used to define grid gutters. 50 | * @param {string} marginCssVar - CSS variable used to define grid margins. 51 | * 52 | */ 53 | function setGridHelperStyles($container, gutterCssVar, marginCssVar) { 54 | const elStyles = $container.style; 55 | elStyles.zIndex = '10000'; 56 | elStyles.position = 'fixed'; 57 | elStyles.top = '0'; 58 | elStyles.left = '0'; 59 | elStyles.display = 'flex'; 60 | elStyles.width = '100%'; 61 | elStyles.height = '100%'; 62 | elStyles.columnGap = `var(${gutterCssVar}, 0)`; 63 | elStyles.paddingLeft = `var(${marginCssVar}, 0)`; 64 | elStyles.paddingRight = `var(${marginCssVar}, 0)`; 65 | elStyles.pointerEvents = 'none'; 66 | elStyles.visibility = 'hidden'; 67 | } 68 | 69 | /** 70 | * Set grid columns 71 | * 72 | * @param {HTMLElement} $container - DOM Element that will contain a list of generated columns 73 | * @param {string} rgbaColor - RGBA color to stylize the generated columns 74 | * 75 | */ 76 | function setGridHelperColumns($container, rgbaColor) { 77 | // Clear columns 78 | $container.innerHTML = ''; 79 | 80 | // Loop through columns 81 | const columns = Number( 82 | window.getComputedStyle($container).getPropertyValue('--grid-columns') 83 | ); 84 | 85 | let $col; 86 | for (var i = 0; i < columns; i++) { 87 | $col = document.createElement('div'); 88 | $col.style.flex = '1 1 0'; 89 | $col.style.backgroundColor = rgbaColor; 90 | $container.appendChild($col); 91 | } 92 | } 93 | 94 | /** 95 | * Set grid events 96 | * 97 | * Resize to rebuild columns 98 | * Keydown/Keyup to toggle the grid display 99 | * 100 | * @param {HTMLElement} $container - DOM Element that contains a list of generated columns 101 | * @param {string} rgbaColor - RGBA color to stylize the generated columns 102 | * 103 | */ 104 | function setGridEvents($container, rgbaColor) { 105 | // Handle resize 106 | window.addEventListener( 107 | 'resize', 108 | setGridHelperColumns($container, rgbaColor) 109 | ); 110 | 111 | // Toggle grid 112 | let ctrlDown = false; 113 | let isActive = false; 114 | 115 | document.addEventListener('keydown', (e) => { 116 | if (e.key == 'Control') { 117 | ctrlDown = true; 118 | } else { 119 | if (ctrlDown && e.key == 'g') { 120 | if (isActive) { 121 | $container.style.visibility = 'hidden'; 122 | } else { 123 | $container.style.visibility = 'visible'; 124 | } 125 | 126 | isActive = !isActive; 127 | } 128 | } 129 | }); 130 | 131 | document.addEventListener('keyup', (e) => { 132 | if (e.key == 'Control') { 133 | ctrlDown = false; 134 | } 135 | }); 136 | } 137 | 138 | export { gridHelper }; 139 | -------------------------------------------------------------------------------- /assets/scripts/utils/html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Escape HTML string 3 | * @param {string} str - string to escape 4 | * @return {string} escaped string 5 | */ 6 | 7 | const escapeHtml = str => 8 | str.replace(/[&<>'"]/g, tag => ({ 9 | '&': '&', 10 | '<': '<', 11 | '>': '>', 12 | "'": ''', 13 | '"': '"' 14 | }[tag])) 15 | 16 | 17 | /** 18 | * Unescape HTML string 19 | * @param {string} str - string to unescape 20 | * @return {string} unescaped string 21 | */ 22 | 23 | const unescapeHtml = str => 24 | str.replace('&', '&') 25 | .replace('<', '<') 26 | .replace('>', '>') 27 | .replace(''', "'") 28 | .replace('"', '"') 29 | 30 | 31 | /** 32 | * Get element data attributes 33 | * @param {HTMLElement} node - node element 34 | * @return {array} node data 35 | */ 36 | 37 | const getNodeData = node => { 38 | 39 | // All attributes 40 | const attributes = node.attributes 41 | 42 | // Regex Pattern 43 | const pattern = /^data\-(.+)$/ 44 | 45 | // Output 46 | const data = {} 47 | 48 | for (let i in attributes) { 49 | if (!attributes[i]) { 50 | continue 51 | } 52 | 53 | // Attributes name (ex: data-module) 54 | let name = attributes[i].name 55 | 56 | // This happens. 57 | if (!name) { 58 | continue 59 | } 60 | 61 | let match = name.match(pattern) 62 | if (!match) { 63 | continue 64 | } 65 | 66 | // If this throws an error, you have some 67 | // serious problems in your HTML. 68 | data[match[1]] = getData(node.getAttribute(name)) 69 | } 70 | 71 | return data; 72 | 73 | } 74 | 75 | 76 | 77 | 78 | /** 79 | * Parse value to data type. 80 | * 81 | * @link https://github.com/jquery/jquery/blob/3.1.1/src/data.js 82 | * @param {string} data - value to convert 83 | * @return {mixed} value in its natural data type 84 | */ 85 | 86 | const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/ 87 | const getData = data => { 88 | if (data === 'true') { 89 | return true 90 | } 91 | 92 | if (data === 'false') { 93 | return false 94 | } 95 | 96 | if (data === 'null') { 97 | return null 98 | } 99 | 100 | // Only convert to a number if it doesn't change the string 101 | if (data === +data+'') { 102 | return +data 103 | } 104 | 105 | if (rbrace.test(data)) { 106 | return JSON.parse(data) 107 | } 108 | 109 | return data 110 | } 111 | 112 | 113 | /** 114 | * Returns an array containing all the parent nodes of the given node 115 | * @param {HTMLElement} $el - DOM Element 116 | * @return {array} parent nodes 117 | */ 118 | 119 | const getParents = $el => { 120 | 121 | // Set up a parent array 122 | let parents = [] 123 | 124 | // Push each parent element to the array 125 | for (; $el && $el !== document; $el = $el.parentNode) { 126 | parents.push($el) 127 | } 128 | 129 | // Return our parent array 130 | return parents 131 | } 132 | 133 | 134 | export { 135 | escapeHtml, 136 | unescapeHtml, 137 | getNodeData, 138 | getData, 139 | getParents, 140 | } 141 | -------------------------------------------------------------------------------- /assets/scripts/utils/image.js: -------------------------------------------------------------------------------- 1 | import { CSS_CLASS } from '../config' 2 | 3 | /** 4 | * Get an image meta data 5 | * 6 | * @param {HTMLImageElement} $img - The image element. 7 | * @return {object} The given image meta data 8 | */ 9 | 10 | const getImageMetadata = $img => ({ 11 | url: $img.src, 12 | width: $img.naturalWidth, 13 | height: $img.naturalHeight, 14 | ratio: $img.naturalWidth / $img.naturalHeight, 15 | }) 16 | 17 | 18 | /** 19 | * Load the given image. 20 | * 21 | * @param {string} url - The URI to lazy load into $el. 22 | * @param {object} options - An object of options 23 | * @return {void} 24 | */ 25 | 26 | const loadImage = (url, options = {}) => { 27 | return new Promise((resolve, reject) => { 28 | const $img = new Image() 29 | 30 | if (options.crossOrigin) { 31 | $img.crossOrigin = options.crossOrigin 32 | } 33 | 34 | const loadCallback = () => { 35 | resolve({ 36 | element: $img, 37 | ...getImageMetadata($img), 38 | }) 39 | } 40 | 41 | if($img.decode) { 42 | $img.src = url 43 | $img.decode().then(loadCallback).catch(e => { 44 | reject(e) 45 | }) 46 | } else { 47 | $img.onload = loadCallback 48 | $img.onerror = (e) => { 49 | reject(e) 50 | } 51 | $img.src = url 52 | } 53 | }) 54 | } 55 | 56 | 57 | /** 58 | * Lazy load the given image. 59 | * 60 | * @param {HTMLImageElement} $el - The image element. 61 | * @param {?string} url - The URI to lazy load into $el. 62 | * If falsey, the value of the `data-src` attribute on $el will be used as the URI. 63 | * @param {?function} callback - A function to call when the image is loaded. 64 | * @return {void} 65 | */ 66 | 67 | const LAZY_LOADED_IMAGES = [] 68 | const lazyLoadImage = async ($el, url, callback) => { 69 | let src = url ? url : $el.dataset.src 70 | 71 | let loadedImage = LAZY_LOADED_IMAGES.find(image => image.url === src) 72 | 73 | if (!loadedImage) { 74 | loadedImage = await loadImage(src) 75 | 76 | if (!loadedImage.url) { 77 | return 78 | } 79 | 80 | LAZY_LOADED_IMAGES.push(loadedImage) 81 | } 82 | 83 | if($el.src === src) { 84 | return 85 | } 86 | 87 | if ($el.tagName === 'IMG') { 88 | $el.src = loadedImage.url 89 | } else { 90 | $el.style.backgroundImage = `url(${loadedImage.url})` 91 | } 92 | 93 | requestAnimationFrame(() => { 94 | let lazyParent = $el.closest(`.${CSS_CLASS.LAZY_CONTAINER}`) 95 | 96 | if(lazyParent) { 97 | lazyParent.classList.add(CSS_CLASS.LAZY_LOADED) 98 | lazyParent.style.backgroundImage = '' 99 | } 100 | 101 | $el.classList.add(CSS_CLASS.LAZY_LOADED) 102 | 103 | callback?.() 104 | }) 105 | } 106 | 107 | 108 | export { 109 | getImageMetadata, 110 | loadImage, 111 | lazyLoadImage 112 | } 113 | -------------------------------------------------------------------------------- /assets/scripts/utils/is.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines if the argument is object-like. 3 | * 4 | * A value is object-like if it's not `null` and has a `typeof` result of "object". 5 | * 6 | * @param {*} x - The value to be checked. 7 | * @return {boolean} 8 | */ 9 | 10 | const isObject = x => (x && typeof x === 'object') 11 | 12 | /** 13 | * Determines if the argument is a function. 14 | * 15 | * @param {*} x - The value to be checked. 16 | * @return {boolean} 17 | */ 18 | 19 | const isFunction = x => typeof x === 'function' 20 | 21 | 22 | export { 23 | isObject, 24 | isFunction 25 | } 26 | -------------------------------------------------------------------------------- /assets/scripts/utils/maths.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamp value 3 | * @param {number} min - start value 4 | * @param {number} max - end value 5 | * @param {number} a - alpha value 6 | * @return {number} clamped value 7 | */ 8 | 9 | const clamp = (min = 0, max = 1, a) => Math.min(max, Math.max(min, a)) 10 | 11 | 12 | /** 13 | * Calculate lerp 14 | * @param {number} x - start value 15 | * @param {number} y - end value 16 | * @param {number} a - alpha value 17 | * @return {number} lerp value 18 | */ 19 | 20 | const lerp = (x, y, a) => x * (1 - a) + y * a 21 | 22 | 23 | /** 24 | * Calculate inverted lerp 25 | * @param {number} x - start value 26 | * @param {number} y - end value 27 | * @param {number} a - alpha value 28 | * @return {number} inverted lerp value 29 | */ 30 | 31 | const invlerp = (x, y, a) => clamp((a - x)/(y - x)) 32 | 33 | 34 | /** 35 | * Round number to the specified precision. 36 | * 37 | * This function is necessary because `Number.prototype.toPrecision()` 38 | * and `Number.prototype.toFixed()` 39 | * 40 | * @param {number} number - The floating point number to round. 41 | * @param {number} [precision] - The number of digits to appear after the decimal point. 42 | * @return {number} The rounded number. 43 | */ 44 | const roundNumber = (number, precision = 2) => { 45 | return Number.parseFloat(number.toPrecision(precision)); 46 | } 47 | 48 | 49 | export { 50 | clamp, 51 | lerp, 52 | invlerp, 53 | roundNumber 54 | } 55 | -------------------------------------------------------------------------------- /assets/scripts/utils/tickers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a debounced function. 3 | * 4 | * A debounced function delays invoking `callback` until after 5 | * `delay` milliseconds have elapsed since the last time the 6 | * debounced function was invoked. 7 | * 8 | * Useful for behaviour that should only happen _before_ or 9 | * _after_ an event has stopped occurring. 10 | * 11 | * @template {function} T 12 | * 13 | * @param {T} callback - The function to debounce. 14 | * @param {number} delay - The number of milliseconds to wait. 15 | * @param {boolean} [immediate] - 16 | * If `true`, `callback` is invoked before `delay`. 17 | * If `false`, `callback` is invoked after `delay`. 18 | * @return {function} The new debounced function. 19 | */ 20 | 21 | const debounce = (callback, delay, immediate = false) => { 22 | let timeout = null 23 | 24 | return (...args) => { 25 | clearTimeout(timeout) 26 | 27 | const later = () => { 28 | timeout = null 29 | if (!immediate) { 30 | callback(...args) 31 | } 32 | } 33 | 34 | if (immediate && !timeout) { 35 | callback(...args) 36 | } 37 | 38 | timeout = setTimeout(later, delay) 39 | } 40 | } 41 | 42 | 43 | /** 44 | * Creates a throttled function. 45 | * 46 | * A throttled function invokes `callback` at most once per every 47 | * `delay` milliseconds. 48 | * 49 | * Useful for rate-limiting an event that occurs in quick succession. 50 | * 51 | * @template {function} T 52 | * 53 | * @param {T} callback - The function to throttle. 54 | * @param {number} delay - The number of milliseconds to wait. 55 | * @return {function} The new throttled function. 56 | */ 57 | 58 | const throttle = (callback, delay) => { 59 | let timeout = false 60 | 61 | return (...args) => { 62 | if (!timeout) { 63 | timeout = true 64 | 65 | callback(...args) 66 | 67 | setTimeout(() => { 68 | timeout = false 69 | }, delay) 70 | } 71 | } 72 | } 73 | 74 | 75 | export { 76 | debounce, 77 | throttle 78 | } 79 | -------------------------------------------------------------------------------- /assets/scripts/utils/transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get translate function 3 | * @param {HTMLElement} $el - DOM Element 4 | * @return {number|object} translate value 5 | */ 6 | 7 | const getTranslate = $el => { 8 | 9 | if(!window.getComputedStyle) { 10 | return 11 | } 12 | 13 | let translate 14 | const style = getComputedStyle($el) 15 | const transform = style.msTransform || style.webkitTransform || style.MozTransform || style.OTransform || style.transform 16 | 17 | const matrix3D = transform.match(/^matrix3d\((.+)\)$/) 18 | if(matrix3D) { 19 | translate = parseFloat(matrix3D[1].split(', ')[13]) 20 | } else { 21 | const matrix = transform.match(/^matrix\((.+)\)$/) 22 | translate = { 23 | x: matrix ? parseFloat(matrix[1].split(', ')[4]) : 0 24 | y: matrix ? parseFloat(matrix[1].split(', ')[5]) : 0 25 | } 26 | } 27 | 28 | return translate 29 | } 30 | 31 | 32 | export { 33 | transform, 34 | getTranslate 35 | } 36 | -------------------------------------------------------------------------------- /assets/scripts/utils/visibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The `PageVisibility` interface provides support for dispatching 3 | * a custom event derived from the value of {@see document.visibilityState} 4 | * when the "visibilitychange" event is fired. 5 | * 6 | * The custom events are: 7 | * 8 | * - "visibilityhidden" representing the "hidden" visibility state. 9 | * - "visibilityvisible" representing the "visibile" visibility state. 10 | * 11 | * Example: 12 | * 13 | * ```js 14 | * import pageVisibility from './utils/visibility.js'; 15 | * 16 | * pageVisibility.enableCustomEvents(); 17 | * 18 | * document.addEventListener('visibilityhidden', () => videoElement.pause()); 19 | * ``` 20 | * 21 | * The dispatched event object is the same from "visibilitychange" 22 | * and renamed according to the visibility state. 23 | * 24 | * The `PageVisibility` interface does not manage the attachment/detachment 25 | * of event listeners on the custom event types. 26 | * 27 | * Further reading: 28 | * 29 | * - {@link https://www.w3.org/TR/page-visibility/ W3 Specification} 30 | * - {@link https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API MDN Web Docs} 31 | */ 32 | export default new class PageVisibility { 33 | /** 34 | * Checks if the "visibilitychange" event listener has been registered. 35 | * 36 | * @return {boolean} Returns `false` if the event listener is not registered, 37 | * otherwise returns `true`. 38 | */ 39 | get isEnabled() { 40 | return isVisibilityChangeObserved; 41 | } 42 | 43 | /** 44 | * Removes the "visibilitychange" event listener. 45 | * 46 | * @return {boolean} Returns `false` if the event listener was already unregistered, 47 | * otherwise returns `true`. 48 | */ 49 | disableCustomEvents() { 50 | if (isVisibilityChangeObserved) { 51 | isVisibilityChangeObserved = false; 52 | document.removeEventListener('visibilitychange', handleCustomVisibilityChange); 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | /** 60 | * Registers the "visibilitychange" event listener. 61 | * 62 | * @return {boolean} Returns `false` if the event listener was already registered, 63 | * otherwise returns `true`. 64 | */ 65 | enableCustomEvents() { 66 | if (!isVisibilityChangeObserved) { 67 | isVisibilityChangeObserved = true; 68 | document.addEventListener('visibilitychange', handleCustomVisibilityChange); 69 | return true; 70 | } 71 | 72 | return false; 73 | } 74 | } 75 | 76 | /** 77 | * Tracks whether custom visibility event types 78 | * are available (`true`) or not (`false`). 79 | * 80 | * @type {boolean} 81 | */ 82 | let isVisibilityChangeObserved = false; 83 | 84 | /** 85 | * Dispatches a custom visibility event at the document derived 86 | * from the value of {@see document.visibilityState}. 87 | * 88 | * @listens document#visibilitychange 89 | * 90 | * @fires PageVisibility#visibilityhidden 91 | * @fires PageVisibility#visibilityvisible 92 | * 93 | * @param {Event} event 94 | * @return {void} 95 | */ 96 | function handleCustomVisibilityChange(event) { 97 | document.dispatchEvent(new CustomEvent(`visibility${document.visibilityState}`, { 98 | detail: { 99 | cause: event 100 | } 101 | })); 102 | } 103 | 104 | /** 105 | * The "visibilityhidden" eveent is fired at the document when the contents 106 | * of its tab have become hidden. 107 | * 108 | * @event PageVisibility#visibilityhidden 109 | * @type {Event} 110 | */ 111 | 112 | /** 113 | * The "visibilityvisible" eveent is fired at the document when the contents 114 | * of its tab have become visible. 115 | * 116 | * @event PageVisibility#visibilityvisible 117 | * @type {Event} 118 | */ 119 | -------------------------------------------------------------------------------- /assets/scripts/vendors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/assets/scripts/vendors/.gitkeep -------------------------------------------------------------------------------- /assets/styles/components/_button.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Components / Buttons 3 | // ========================================================================== 4 | 5 | .c-button { 6 | padding: rem(15px) rem(20px); 7 | background-color: lightgray; 8 | 9 | @include u-hocus { 10 | background-color: darkgray; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/styles/components/_form.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Components / Form 3 | // ========================================================================== 4 | 5 | .c-form { 6 | 7 | } 8 | 9 | .c-form_item { 10 | position: relative; 11 | margin-bottom: rem(30px); 12 | } 13 | 14 | // Label 15 | // ========================================================================== 16 | 17 | .c-form_label { 18 | display: block; 19 | margin-bottom: rem(10px); 20 | } 21 | 22 | // Input 23 | // ========================================================================== 24 | 25 | $input-icon-color: 424242; // No # 26 | 27 | .c-form_input { 28 | padding: rem(10px); 29 | border: 1px solid lightgray; 30 | background-color: colorCode(lightest); 31 | 32 | &:hover { 33 | border-color: darkgray; 34 | } 35 | 36 | &:focus { 37 | border-color: dimgray; 38 | } 39 | 40 | &::placeholder { 41 | color: gray; 42 | } 43 | } 44 | 45 | // Checkbox 46 | // ========================================================================== 47 | 48 | $checkbox: rem(18px); 49 | $checkbox-icon-color: $input-icon-color; 50 | 51 | .c-form_checkboxLabel { 52 | @extend .c-form_label; 53 | 54 | position: relative; 55 | display: inline-block; 56 | margin-right: rem(10px); 57 | margin-bottom: 0; 58 | padding-left: ($checkbox + rem(10px)); 59 | cursor: pointer; 60 | 61 | &::before, &::after { 62 | position: absolute; 63 | top: 50%; 64 | left: 0; 65 | display: inline-block; 66 | margin-top: math.div(-$checkbox, 2); 67 | padding: 0; 68 | width: $checkbox; 69 | height: $checkbox; 70 | content: ""; 71 | } 72 | 73 | &::before { 74 | background-color: colorCode(lightest); 75 | border: 1px solid lightgray; 76 | } 77 | 78 | &::after { 79 | border-color: transparent; 80 | background-color: transparent; 81 | background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20version%3D%221.1%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2213%22%20height%3D%2210.5%22%20viewBox%3D%220%200%2013%2010.5%22%20enable-background%3D%22new%200%200%2013%2010.5%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpath%20fill%3D%22%23#{$checkbox-icon-color}%22%20d%3D%22M4.8%205.8L2.4%203.3%200%205.7l4.8%204.8L13%202.4c0%200-2.4-2.4-2.4-2.4L4.8%205.8z%22%2F%3E%3C%2Fsvg%3E"); 82 | background-position: center; 83 | background-size: rem(12px); 84 | background-repeat: no-repeat; 85 | opacity: 0; 86 | } 87 | 88 | &:hover { 89 | &::before { 90 | border-color: darkgray; 91 | } 92 | } 93 | 94 | .c-form_checkbox:focus + & { 95 | &::before { 96 | border-color: dimgray; 97 | } 98 | } 99 | 100 | .c-form_checkbox:checked + & { 101 | &::after { 102 | opacity: 1; 103 | } 104 | } 105 | } 106 | 107 | .c-form_checkbox { 108 | position: absolute; 109 | width: 0; 110 | opacity: 0; 111 | } 112 | 113 | // Radio 114 | // ========================================================================== 115 | 116 | $radio-icon-color: $input-icon-color; 117 | 118 | .c-form_radioLabel { 119 | @extend .c-form_checkboxLabel; 120 | 121 | &::before, &::after { 122 | border-radius: 50%; 123 | } 124 | 125 | &::after { 126 | background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20version%3D%221.1%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20enable-background%3D%22new%200%200%2013%2013%22%20xml%3Aspace%3D%22preserve%22%3E%3Ccircle%20fill%3D%22%23#{$radio-icon-color}%22%20cx%3D%226.5%22%20cy%3D%226.5%22%20r%3D%226.5%22%2F%3E%3C%2Fsvg%3E"); 127 | background-size: rem(6px); 128 | } 129 | } 130 | 131 | .c-form_radio { 132 | @extend .c-form_checkbox; 133 | } 134 | 135 | // Select 136 | // ============================================================================= 137 | 138 | $select-icon: rem(40px); 139 | $select-icon-color: $input-icon-color; 140 | 141 | .c-form_select { 142 | position: relative; 143 | cursor: pointer; 144 | 145 | &::after { 146 | position: absolute; 147 | top: 0; 148 | right: 0; 149 | bottom: 0; 150 | z-index: 2; 151 | width: $select-icon; 152 | background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20version%3D%221.1%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2213%22%20height%3D%2211.3%22%20viewBox%3D%220%200%2013%2011.3%22%20enable-background%3D%22new%200%200%2013%2011.3%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpolygon%20fill%3D%22%23#{$select-icon-color}%22%20points%3D%226.5%2011.3%203.3%205.6%200%200%206.5%200%2013%200%209.8%205.6%20%22%2F%3E%3C%2Fsvg%3E"); 153 | background-position: center; 154 | background-size: rem(8px); 155 | background-repeat: no-repeat; 156 | content: ""; 157 | pointer-events: none; 158 | } 159 | } 160 | 161 | .c-form_select_input { 162 | @extend .c-form_input; 163 | 164 | position: relative; 165 | z-index: 1; 166 | padding-right: $select-icon; 167 | cursor: pointer; 168 | } 169 | 170 | // Textarea 171 | // ============================================================================= 172 | 173 | .c-form_textarea { 174 | @extend .c-form_input; 175 | 176 | min-height: rem(200px); 177 | } 178 | -------------------------------------------------------------------------------- /assets/styles/components/_heading.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Components / Headings 3 | // ========================================================================== 4 | 5 | // Font sizes 6 | // ========================================================================== 7 | :root { 8 | // Default 9 | --font-size-h1: #{responsive-value(38px, 90px, $from-xl)}; 10 | --font-size-h2: #{responsive-value(34px, 72px, $from-xl)}; 11 | --font-size-h3: #{responsive-value(28px, 54px, $from-xl)}; 12 | --font-size-h4: #{responsive-value(24px, 40px, $from-xl)}; 13 | --font-size-h5: #{responsive-value(20px, 30px, $from-xl)}; 14 | --font-size-h6: #{responsive-value(18px, 23px, $from-xl)}; 15 | } 16 | 17 | // Mixins 18 | // ========================================================================== 19 | 20 | @mixin heading { 21 | font-family: ff('sans'); 22 | font-weight: $font-weight-medium; 23 | } 24 | 25 | @mixin heading-h1 { 26 | font-size: var(--font-size-h1); 27 | line-height: 1.1; 28 | } 29 | 30 | @mixin heading-h2 { 31 | font-size: var(--font-size-h2); 32 | line-height: 1.1; 33 | } 34 | 35 | @mixin heading-h3 { 36 | font-size: var(--font-size-h3); 37 | line-height: 1.1; 38 | } 39 | 40 | @mixin heading-h4 { 41 | font-size: var(--font-size-h4); 42 | line-height: 1.2; 43 | } 44 | 45 | @mixin heading-h5 { 46 | font-size: var(--font-size-h5); 47 | line-height: 1.2; 48 | } 49 | 50 | @mixin heading-h6 { 51 | font-size: var(--font-size-h6); 52 | line-height: 1.4; 53 | } 54 | 55 | // Styles 56 | // ========================================================================== 57 | 58 | .c-heading { 59 | @include heading; 60 | 61 | &.-h1 { 62 | @include heading-h1; 63 | } 64 | &.-h2 { 65 | @include heading-h2; 66 | } 67 | &.-h3 { 68 | @include heading-h3; 69 | } 70 | &.-h4 { 71 | @include heading-h4; 72 | } 73 | &.-h5 { 74 | @include heading-h5; 75 | } 76 | &.-h6 { 77 | @include heading-h6; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /assets/styles/components/_scrollbar.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Components / Scrollbar 3 | // ========================================================================== 4 | 5 | .c-scrollbar { 6 | position: absolute; 7 | right: 0; 8 | top: 0; 9 | width: 11px; 10 | height: 100vh; 11 | transform-origin: center right; 12 | transition: transform t(normal), opacity t(normal); 13 | opacity: 0; 14 | 15 | &:hover { 16 | transform: scaleX(1.45); 17 | } 18 | 19 | &:hover, .has-scroll-scrolling &, .has-scroll-dragging & { 20 | opacity: 1; 21 | } 22 | } 23 | 24 | .c-scrollbar_thumb { 25 | position: absolute; 26 | top: 0; 27 | right: 0; 28 | background-color: colorCode(darkest); 29 | opacity: 0.5; 30 | width: 7px; 31 | border-radius: 10px; 32 | margin: 2px; 33 | cursor: grab; 34 | 35 | .has-scroll-dragging & { 36 | cursor: grabbing; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/styles/components/_text.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Components / Texts 3 | // ========================================================================== 4 | 5 | // Font sizes 6 | // ========================================================================== 7 | :root { 8 | --font-size-body-regular: #{responsive-value(15px, 17px, $from-lg)}; 9 | --font-size-body-medium: #{responsive-value(18px, 23px, $from-lg)}; 10 | --font-size-body-small: #{responsive-value(13px, 16px, $from-lg)}; 11 | } 12 | 13 | // Mixins 14 | // ========================================================================== 15 | @mixin text { 16 | font-family: ff('sans'); 17 | } 18 | 19 | @mixin body-regular { 20 | font-size: var(--font-size-body-regular); 21 | font-weight: $font-weight-normal; 22 | line-height: 1.2; 23 | } 24 | 25 | @mixin body-medium { 26 | font-size: var(--font-size-body-medium); 27 | font-weight: $font-weight-normal; 28 | line-height: 1.2; 29 | } 30 | 31 | @mixin body-small { 32 | font-size: var(--font-size-body-small); 33 | font-weight: $font-weight-normal; 34 | line-height: 1.2; 35 | } 36 | 37 | // Styles 38 | // ========================================================================== 39 | .c-text { 40 | @include text; 41 | 42 | &.-body-regular { 43 | @include body-regular; 44 | } 45 | 46 | &.-body-medium { 47 | @include body-medium; 48 | } 49 | 50 | &.-body-small { 51 | @include body-small; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /assets/styles/critical.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Critical CSS 3 | // ========================================================================== 4 | 5 | $assets-path: "assets/"; 6 | -------------------------------------------------------------------------------- /assets/styles/elements/_document.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Elements / Document 3 | // ========================================================================== 4 | 5 | // 6 | // Simple page-level setup. 7 | // 8 | // 1. Includes fonts 9 | // 2. Ensure the page always fills at least the entire height of the viewport. 10 | // 3. Set the default `font-size` and `line-height` for the entire project, 11 | // sourced from our default variables. 12 | 13 | @include font-faces($font-faces, $font-dir); // [1] 14 | 15 | html { 16 | min-height: 100%; // [2] 17 | line-height: $line-height; // [3] 18 | font-family: ff("sans"); 19 | color: $font-color; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | 23 | @media (max-width: $to-sm) { 24 | font-size: $font-size - 2px; 25 | } 26 | 27 | @media (min-width: $from-sm) and (max-width: $to-lg) { 28 | font-size: $font-size - 1px; 29 | } 30 | 31 | @media (min-width: $from-lg) and (max-width: $to-2xl) { 32 | font-size: $font-size; 33 | } 34 | 35 | @media (min-width: $from-2xl) and (max-width: $to-3xl) { 36 | font-size: $font-size + 1px; 37 | } 38 | 39 | @media (min-width: $from-3xl) { 40 | font-size: $font-size + 2px; 41 | } 42 | 43 | &.is-loading { 44 | cursor: wait; 45 | } 46 | } 47 | 48 | body { 49 | } 50 | 51 | ::selection { 52 | background-color: $color-selection-background; 53 | color: $color-selection-text; 54 | text-shadow: none; 55 | } 56 | 57 | a { 58 | color: $color-link; 59 | 60 | @include u-hocus { 61 | color: $color-link-hover; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /assets/styles/elements/_normalize.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Elements / Normalize 3 | // ========================================================================== 4 | 5 | // Modern CSS Normalize 6 | // Based on the reset by Andy.set with some tweaks. 7 | // Original by Andy.set: https://piccalil.li/blog/a-more-modern-css-reset/ 8 | // Review by Chris collier: https://chriscoyier.net/2023/10/03/being-picky-about-a-css-reset-for-fun-pleasure/ 9 | 10 | 11 | // Box sizing rules 12 | *, 13 | *:after, 14 | *:before { 15 | box-sizing: border-box; 16 | } 17 | 18 | // Prevent font size inflation 19 | html { 20 | -moz-text-size-adjust: none; 21 | -webkit-text-size-adjust: none; 22 | text-size-adjust: none; 23 | } 24 | 25 | // Remove default margin in favour of better control in authored CSS 26 | p, 27 | h1, 28 | h2, 29 | h3, 30 | h4, 31 | h5, 32 | h6, 33 | dl, 34 | dd, 35 | figure, 36 | blockquote { 37 | margin-block: unset; 38 | } 39 | 40 | // Remove list styles on ul, ol elements with a class, which suggests default styling will be removed 41 | ul[class], 42 | ol[class] { 43 | margin: 0; 44 | padding: 0; 45 | list-style: none; 46 | } 47 | 48 | // Set core defaults 49 | html { 50 | line-height: 1.5; 51 | } 52 | 53 | body { 54 | margin: unset; 55 | } 56 | 57 | // Set shorter line heights on headings and interactive elements 58 | h1, 59 | h2, 60 | h3, 61 | h4, 62 | h5, 63 | h6, 64 | input, 65 | label, 66 | button { 67 | line-height: 1.1; 68 | } 69 | 70 | // Balance text wrapping on headings 71 | h1, 72 | h2, 73 | h3, 74 | h4, 75 | h5, 76 | h6 { 77 | text-wrap: balance; 78 | } 79 | 80 | // Remove a elements default styles if they have a class 81 | a[class] { 82 | color: inherit; 83 | text-decoration: none; 84 | } 85 | 86 | // Make assets easier to work with 87 | img, 88 | svg, 89 | canvas, 90 | picture { 91 | display: block; 92 | max-inline-size: 100%; 93 | block-size: auto; 94 | } 95 | 96 | // Inherit fonts for inputs and buttons 97 | input, 98 | button, 99 | select, 100 | textarea { 101 | font: inherit; 102 | } 103 | 104 | // Make sure textareas without a rows attribute are not tiny 105 | textarea:not([rows]) { 106 | min-height: 10em; 107 | } 108 | 109 | // Anything that has been anchored to should have extra scroll margin 110 | :target { 111 | scroll-margin-block: 1rlh; 112 | } 113 | 114 | // Reduced mootion preference 115 | @media (prefers-reduced-motion: reduce) { 116 | *, 117 | *:after, 118 | *:before { 119 | animation-duration: 0.01ms !important; 120 | animation-iteration-count: 1 !important; 121 | transition-duration: 0.01ms !important; 122 | scroll-behavior: auto !important; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Main 3 | // ========================================================================== 4 | 5 | // Modules 6 | // ========================================================================== 7 | 8 | @use "sass:math"; 9 | 10 | // Tools 11 | // ========================================================================== 12 | 13 | @import "tools/maths"; 14 | @import "tools/functions"; 15 | @import "tools/mixins"; 16 | // @import "tools/layout"; 17 | // @import "tools/widths"; 18 | // @import "tools/family"; 19 | 20 | // Settings 21 | // ========================================================================== 22 | 23 | @import "settings/config"; 24 | @import "settings/config.breakpoints"; 25 | @import "settings/config.colors"; 26 | @import "settings/config.eases"; 27 | @import "settings/config.fonts"; 28 | @import "settings/config.spacings"; 29 | @import "settings/config.speeds"; 30 | @import "settings/config.zindexes"; 31 | @import "settings/config.variables"; 32 | 33 | // Vendors 34 | // ========================================================================== 35 | @import "../../node_modules/locomotive-scroll/dist/locomotive-scroll"; 36 | 37 | // Elements 38 | // ========================================================================== 39 | 40 | @import "elements/normalize"; 41 | @import "elements/document"; 42 | 43 | // Objects 44 | // ========================================================================== 45 | 46 | @import "objects/container"; 47 | @import "objects/ratio"; 48 | @import "objects/icons"; 49 | @import "objects/grid"; 50 | // @import "objects/layout"; 51 | // @import "objects/table"; 52 | 53 | // Components 54 | // ========================================================================== 55 | 56 | @import "components/heading"; 57 | @import "components/text"; 58 | @import "components/button"; 59 | @import "components/form"; 60 | 61 | // Utilities 62 | // ========================================================================== 63 | 64 | @import "utilities/ratio"; 65 | @import "utilities/grid-column"; 66 | // @import "utilities/widths"; 67 | // @import "utilities/align"; 68 | // @import "utilities/helpers"; 69 | // @import "utilities/states"; 70 | @import "utilities/spacing"; 71 | // @import "utilities/print"; 72 | -------------------------------------------------------------------------------- /assets/styles/objects/_container.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Objects / Container 3 | // ========================================================================== 4 | 5 | // Page-level constraining and wrapping elements. 6 | // 7 | // > In programming languages the word *container* is generally used for structures 8 | // that can contain more than one element. 9 | // > A *wrapper* instead is something that wraps around a single object to provide 10 | // more functionalities and interfaces to it. 11 | // @link http://stackoverflow.com/a/13202141/140357 12 | 13 | .o-container { 14 | margin-right: auto; 15 | margin-left: auto; 16 | padding-left: var(--grid-margin); 17 | padding-right: var(--grid-margin); 18 | } 19 | -------------------------------------------------------------------------------- /assets/styles/objects/_grid.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Grid helper 3 | // ========================================================================== 4 | // Help: https://css-tricks.com/snippets/css/complete-guide-grid/ 5 | // 6 | /** 7 | * Usage: 8 | * 9 | * ```html 10 | *
11 | *
12 | *

Hello

13 | *
14 | *
15 | *

Hello

16 | *
17 | *
18 | * ``` 19 | */ 20 | 21 | .o-grid { 22 | display: grid; 23 | width: 100%; 24 | 25 | &:is(ul, ol) { 26 | margin: 0; 27 | padding: 0; 28 | list-style: none; 29 | } 30 | 31 | // ========================================================================== 32 | // Cols 33 | // ========================================================================== 34 | 35 | // Responsive grid columns based on `--grid-columns` 36 | &.-cols { 37 | grid-template-columns: repeat(var(--grid-columns), 1fr); 38 | } 39 | 40 | &.-col-#{$base-column-nb} { 41 | grid-template-columns: repeat(#{$base-column-nb}, 1fr); 42 | } 43 | 44 | &.-col-4 { 45 | grid-template-columns: repeat(4, 1fr); 46 | } 47 | 48 | &.-col-#{$base-column-nb}\@from-md { 49 | @media (min-width: $from-md) { 50 | grid-template-columns: repeat(#{$base-column-nb}, 1fr); 51 | } 52 | } 53 | 54 | // ========================================================================== 55 | // Gutters 56 | // ========================================================================== 57 | 58 | // Gutters rows and columns 59 | &.-gutters { 60 | gap: var(--grid-gutter); 61 | column-gap: var(--grid-gutter); 62 | } 63 | 64 | // ========================================================================== 65 | // Modifiers 66 | // ========================================================================== 67 | &.-full-height { 68 | height: 100%; 69 | } 70 | 71 | // ========================================================================== 72 | // Aligns 73 | // ========================================================================== 74 | 75 | // ========================================================================== 76 | // Items inside cells 77 | // 78 | &.-top-items { 79 | align-items: start; 80 | } 81 | &.-right-items { 82 | justify-items: end; 83 | } 84 | &.-bottom-items { 85 | align-items: end; 86 | } 87 | &.-left-items { 88 | justify-items: start; 89 | } 90 | &.-center-items { 91 | align-items: center; 92 | justify-items: center; 93 | } 94 | &.-center-items-x { 95 | justify-items: center; 96 | } 97 | &.-center-items-y { 98 | align-items: center; 99 | } 100 | &.-stretch-items { 101 | align-items: stretch; 102 | justify-items: stretch; 103 | } 104 | 105 | // ========================================================================== 106 | // Cells 107 | // 108 | &.-top-cells { 109 | align-content: start; 110 | } 111 | &.-right-cells { 112 | justify-content: end; 113 | } 114 | &.-bottom-cells { 115 | align-content: end; 116 | } 117 | &.-left-cells { 118 | justify-content: start; 119 | } 120 | &.-center-cells { 121 | align-content: center; 122 | justify-content: center; 123 | } 124 | &.-center-cells-x { 125 | justify-content: center; 126 | } 127 | &.-center-cells-y { 128 | align-content: center; 129 | } 130 | &.-stretch-cells { 131 | align-content: stretch; 132 | justify-content: stretch; 133 | } 134 | &.-space-around-cells { 135 | align-content: space-around; 136 | justify-content: space-around; 137 | } 138 | &.-space-around-cells-x { 139 | justify-content: space-around; 140 | } 141 | &.-space-around-cells-y { 142 | align-content: space-around; 143 | } 144 | &.-space-between-cells { 145 | justify-content: space-between; 146 | align-content: space-between; 147 | } 148 | &.-space-between-cells-x { 149 | justify-content: space-between; 150 | } 151 | &.-space-between-cells-y { 152 | align-content: space-between; 153 | } 154 | &.-space-evenly-cells { 155 | justify-content: space-evenly; 156 | align-content: space-evenly; 157 | } 158 | &.-space-evenly-cells-x { 159 | justify-content: space-evenly; 160 | } 161 | &.-space-evenly-cells-y { 162 | align-content: space-evenly; 163 | } 164 | } 165 | 166 | // ========================================================================== 167 | // Grid item 168 | // ========================================================================== 169 | // By default, a grid item takes full width of its parent. 170 | // 171 | .o-grid_item { 172 | grid-column-start: var(--gc-start, 1); 173 | grid-column-end: var(--gc-end, -1); 174 | 175 | &.-align-end { 176 | align-self: end; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /assets/styles/objects/_icons.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Objects / SVG Icons 3 | // ========================================================================== 4 | 5 | 6 | // Markup 7 | // 8 | // 1. If icon is accessible and has a title 9 | // 2. If icon is decorative 10 | // 11 | // 12 | // 25 | // 26 | 27 | // Global styles for icones 28 | // ========================================================================== 29 | 30 | .o-icon { 31 | display: inline-block; 32 | vertical-align: middle; 33 | 34 | svg { 35 | --icon-height: calc(var(--icon-width) * math.div(1, (var(--icon-ratio)))); 36 | 37 | display: block; 38 | width: var(--icon-width); 39 | height: var(--icon-height); 40 | fill: currentColor; 41 | } 42 | } 43 | 44 | 45 | // SVG sizes 46 | // ========================================================================== 47 | 48 | // // Logo 49 | // .svg-logo { 50 | // --icon-width: #{rem(100px)}; 51 | // --icon-ratio: math.div(20, 30); // width/height based on svg viewBox 52 | 53 | // // Sizes 54 | // .o-icon.-big & { 55 | // --icon-width: #{rem(200px)}; 56 | // } 57 | // } 58 | 59 | -------------------------------------------------------------------------------- /assets/styles/objects/_layout.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Objects / Layout 3 | // ========================================================================== 4 | 5 | //// 6 | /// Grid-like layout system. 7 | /// 8 | /// The layout object provides us with a column-style layout system. This file 9 | /// contains the basic structural elements, but classes should be complemented 10 | /// with width utilities, for example: 11 | /// 12 | /// @example 13 | ///
14 | ///
15 | ///
16 | ///
17 | ///
18 | ///
19 | ///
20 | ///
21 | /// 22 | /// We can also manipulate entire layout systems by adding a series of modifiers 23 | /// to the `.o-layout` block. For example: 24 | /// 25 | /// @example 26 | ///
27 | /// 28 | /// This will reverse the displayed order of the system so that it runs in the 29 | /// opposite order to our source, effectively flipping the system over. 30 | /// 31 | /// @example 32 | ///
33 | /// 34 | /// This will cause the system to fill up from either the centre or the right 35 | /// hand side. Default behaviour is to fill up the layout system from the left. 36 | /// 37 | /// @requires tools/layout 38 | /// @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss 39 | //// 40 | 41 | .o-layout { 42 | @include o-layout; 43 | 44 | // Gutter modifiers 45 | &.-gutter { 46 | margin-left: rem(-$unit); 47 | } 48 | 49 | &.-gutter-small { 50 | margin-left: rem(-$unit-small); 51 | } 52 | 53 | // Horizontal aligment modifiers 54 | &.-center { 55 | text-align: center; 56 | } 57 | 58 | &.-right { 59 | text-align: right; 60 | } 61 | 62 | &.-reverse { 63 | direction: rtl; 64 | 65 | &.-flex { 66 | flex-direction: row-reverse; 67 | } 68 | } 69 | 70 | &.-flex { 71 | display: flex; 72 | 73 | &.-top { 74 | align-items: flex-start; 75 | } 76 | &.-middle { 77 | align-items: center; 78 | } 79 | &.-bottom { 80 | align-items: flex-end; 81 | } 82 | } 83 | &.-stretch { 84 | align-items: stretch; 85 | } 86 | } 87 | 88 | .o-layout_item { 89 | @include o-layout_item; 90 | 91 | // Gutter modifiers 92 | .o-layout.-gutter > & { 93 | padding-left: rem($unit); 94 | } 95 | 96 | .o-layout.-gutter-small > & { 97 | padding-left: rem($unit-small); 98 | } 99 | 100 | // Vertical alignment modifiers 101 | .o-layout.-middle > & { 102 | vertical-align: middle; 103 | } 104 | 105 | .o-layout.-bottom > & { 106 | vertical-align: bottom; 107 | } 108 | 109 | // Horizontal aligment modifiers 110 | .o-layout.-center > &, 111 | .o-layout.-right > &, 112 | .o-layout.-reverse > & { 113 | text-align: left; 114 | } 115 | 116 | .o-layout.-reverse > & { 117 | direction: ltr; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /assets/styles/objects/_ratio.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Objects / Ratio 3 | // ========================================================================== 4 | 5 | // Create ratio-bound content blocks, to keep media (e.g. images, videos) in 6 | // their correct aspect ratios. 7 | // 8 | // http://alistapart.com/article/creating-intrinsic-ratios-for-video 9 | // 10 | // 1. Default cropping is a 1:1 ratio (i.e. a perfect square). 11 | 12 | .o-ratio { 13 | position: relative; 14 | display: block; 15 | overflow: hidden; 16 | 17 | &:before { 18 | display: block; 19 | padding-bottom: 100%; // [1] 20 | width: 100%; 21 | content: ""; 22 | } 23 | } 24 | 25 | .o-ratio_content, 26 | .o-ratio > img, 27 | .o-ratio > iframe, 28 | .o-ratio > embed, 29 | .o-ratio > object { 30 | position: absolute; 31 | top: 0; 32 | bottom: 0; 33 | left: 0; 34 | width: 100%; 35 | // height: 100%; 36 | } 37 | -------------------------------------------------------------------------------- /assets/styles/objects/_table.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Objects / Tables 3 | // ========================================================================== 4 | 5 | .o-table { 6 | width: 100%; 7 | 8 | // Force all cells within a table to occupy the same width as each other. 9 | // 10 | // @link https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout#Values 11 | 12 | &.-fixed { 13 | table-layout: fixed; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.breakpoints.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / Breakpoints 3 | // ========================================================================== 4 | 5 | // Breakpoints 6 | // ========================================================================== 7 | 8 | $breakpoints: ( 9 | "2xs": 340px, 10 | "xs": 500px, 11 | "sm": 700px, 12 | "md": 1000px, 13 | "lg": 1200px, 14 | "xl": 1400px, 15 | "2xl": 1600px, 16 | "3xl": 1800px, 17 | "4xl": 2000px, 18 | "5xl": 2400px 19 | ); 20 | 21 | // Functions 22 | // ========================================================================== 23 | 24 | // Creates a min-width or max-width media query expression. 25 | // 26 | // @param {string} $breakpoint The breakpoint. 27 | // @param {string} $type Either "min" or "max". 28 | // @return {string} 29 | 30 | @function mq($breakpoint, $type: "min") { 31 | @if not map-has-key($breakpoints, $breakpoint) { 32 | @warn "Unknown media query breakpoint: `#{$breakpoint}`"; 33 | } 34 | 35 | $value: map-get($breakpoints, $breakpoint); 36 | 37 | @if ($type == "min") { 38 | @return "(min-width: #{$value})"; 39 | } 40 | @if ($type == "max") { 41 | @return "(max-width: #{$value - 1px})"; 42 | } 43 | 44 | @error "Unknown media query type: #{$type}"; 45 | } 46 | 47 | // Creates a min-width media query expression. 48 | // 49 | // @param {string} $breakpoint The breakpoint. 50 | // @return {string} 51 | 52 | @function mq-min($breakpoint) { 53 | @return mq($breakpoint, "min"); 54 | } 55 | 56 | // Creates a max-width media query expression. 57 | // 58 | // @param {string} $breakpoint The breakpoint. 59 | // @return {string} 60 | 61 | @function mq-max($breakpoint) { 62 | @return mq($breakpoint, "max"); 63 | } 64 | 65 | // Creates a min-width and max-width media query expression. 66 | // 67 | // @param {string} $from The min-width breakpoint. 68 | // @param {string} $until The max-width breakpoint. 69 | // @return {string} 70 | 71 | @function mq-between($breakpointMin, $breakpointMax) { 72 | @return "#{mq-min($breakpointMin)} and #{mq-max($breakpointMax)}"; 73 | } 74 | 75 | 76 | // Legacy 77 | // ========================================================================== 78 | 79 | $from-xs: map-get($breakpoints, "xs") !default; 80 | $to-xs: map-get($breakpoints, "xs") - 1 !default; 81 | $from-sm: map-get($breakpoints, "sm") !default; 82 | $to-sm: map-get($breakpoints, "sm") - 1 !default; 83 | $from-md: map-get($breakpoints, "md") !default; 84 | $to-md: map-get($breakpoints, "md") - 1 !default; 85 | $from-lg: map-get($breakpoints, "lg") !default; 86 | $to-lg: map-get($breakpoints, "lg") - 1 !default; 87 | $from-xl: map-get($breakpoints, "xl") !default; 88 | $to-xl: map-get($breakpoints, "xl") - 1 !default; 89 | $from-2xl: map-get($breakpoints, "2xl") !default; 90 | $to-2xl: map-get($breakpoints, "2xl") - 1 !default; 91 | $from-3xl: map-get($breakpoints, "3xl") !default; 92 | $to-3xl: map-get($breakpoints, "3xl") - 1 !default; 93 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.colors.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | // ========================================================================== 4 | // Settings / Config / Colors 5 | // ========================================================================== 6 | 7 | // Palette 8 | // ========================================================================== 9 | 10 | $colors: ( 11 | primary: #3297FD, 12 | lightest: #FFFFFF, 13 | darkest: #000000, 14 | ); 15 | 16 | // Function 17 | // ========================================================================== 18 | 19 | // Returns color code. 20 | // 21 | // ```scss 22 | // .c-box { 23 | // color: colorCode(primary); 24 | // } 25 | // ``` 26 | // 27 | // @param {string} $key - The color key in $colors. 28 | // @param {number} $alpha - The alpha for the color value. 29 | // @return {color} 30 | 31 | @function colorCode($key, $alpha: 1) { 32 | @if not map-has-key($colors, $key) { 33 | @error "Unknown '#{$key}' in $colors."; 34 | } 35 | 36 | @if($alpha < 0 or $alpha > 1) { 37 | @error "Alpha '#{$alpha}' must be in range [0, 1]."; 38 | } 39 | 40 | $color: map-get($colors, $key); 41 | 42 | @return rgba($color, $alpha); 43 | } 44 | 45 | // Specifics 46 | // ========================================================================== 47 | 48 | // Link 49 | $color-link: colorCode(primary); 50 | $color-link-focus: colorCode(primary); 51 | $color-link-hover: color.adjust(colorCode(primary), $lightness: -10%); 52 | 53 | // Selection 54 | $color-selection-text: colorCode(darkest); 55 | $color-selection-background: colorCode(lightest); 56 | 57 | // Socials 58 | $color-facebook: #3B5998; 59 | $color-instagram: #E1306C; 60 | $color-youtube: #CD201F; 61 | $color-twitter: #1DA1F2; 62 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.eases.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / Eases 3 | // ========================================================================== 4 | 5 | // Eases 6 | // ========================================================================== 7 | 8 | $eases: ( 9 | // Power 1 10 | "power1.in": cubic-bezier(0.550, 0.085, 0.680, 0.530), 11 | "power1.out": cubic-bezier(0.250, 0.460, 0.450, 0.940), 12 | "power1.inOut": cubic-bezier(0.455, 0.030, 0.515, 0.955), 13 | 14 | // Power 2 15 | "power2.in": cubic-bezier(0.550, 0.055, 0.675, 0.190), 16 | "power2.out": cubic-bezier(0.215, 0.610, 0.355, 1.000), 17 | "power2.inOut": cubic-bezier(0.645, 0.045, 0.355, 1.000), 18 | 19 | // Power 3 20 | "power3.in": cubic-bezier(0.895, 0.030, 0.685, 0.220), 21 | "power3.out": cubic-bezier(0.165, 0.840, 0.440, 1.000), 22 | "power3.inOut": cubic-bezier(0.770, 0.000, 0.175, 1.000), 23 | 24 | // Power 4 25 | "power4.in": cubic-bezier(0.755, 0.050, 0.855, 0.060), 26 | "power4.out": cubic-bezier(0.230, 1.000, 0.320, 1.000), 27 | "power4.inOut": cubic-bezier(0.860, 0.000, 0.070, 1.000), 28 | 29 | // Expo 30 | "expo.in": cubic-bezier(0.950, 0.050, 0.795, 0.035), 31 | "expo.out": cubic-bezier(0.190, 1.000, 0.220, 1.000), 32 | "expo.inOut": cubic-bezier(1.000, 0.000, 0.000, 1.000), 33 | 34 | // Back 35 | "back.in": cubic-bezier(0.600, -0.280, 0.735, 0.045), 36 | "back.out": cubic-bezier(0.175, 00.885, 0.320, 1.275), 37 | "back.inOut": cubic-bezier(0.680, -0.550, 0.265, 1.550), 38 | 39 | // Sine 40 | "sine.in": cubic-bezier(0.470, 0.000, 0.745, 0.715), 41 | "sine.out": cubic-bezier(0.390, 0.575, 0.565, 1.000), 42 | "sine.inOut": cubic-bezier(0.445, 0.050, 0.550, 0.950), 43 | 44 | // Circ 45 | "circ.in": cubic-bezier(0.600, 0.040, 0.980, 0.335), 46 | "circ.out": cubic-bezier(0.075, 0.820, 0.165, 1.000), 47 | "circ.inOut": cubic-bezier(0.785, 0.135, 0.150, 0.860), 48 | 49 | // Misc 50 | "bounce": cubic-bezier(0.17, 0.67, 0.3, 1.33), 51 | "slow.out": cubic-bezier(.04,1.15,0.4,.99), 52 | "smooth": cubic-bezier(0.380, 0.005, 0.215, 1), 53 | ); 54 | 55 | // Default value for ease() 56 | $ease-default: "power2.out" !default; 57 | 58 | // Function 59 | // ========================================================================== 60 | 61 | // Returns ease curve. 62 | // 63 | // ```scss 64 | // .c-box { 65 | // transition-timing-function: ease("power2.out"); 66 | // } 67 | // ``` 68 | // 69 | // @param {string} $key - The ease key in $eases. 70 | // @return {easing-function} 71 | 72 | @function ease($key: $ease-default) { 73 | @if not map-has-key($eases, $key) { 74 | @error "Unknown '#{$key}' in $eases."; 75 | } 76 | 77 | @return map-get($eases, $key); 78 | } 79 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.fonts.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / Breakpoints 3 | // ========================================================================== 4 | 5 | // Font fallbacks (retrieved from systemfontstack.com on 2022-05-31) 6 | // ========================================================================== 7 | 8 | $font-fallback-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 9 | $font-fallback-serif: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 10 | $font-fallback-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; 11 | 12 | // Typefaces 13 | // ========================================================================== 14 | 15 | // List of custom font faces as tuples. 16 | // 17 | // ``` 18 | // 19 | // ``` 20 | $font-faces: ( 21 | ("Source Sans", "SourceSans3-Bold", 700, normal), 22 | ("Source Sans", "SourceSans3-BoldIt", 700, italic), 23 | ("Source Sans", "SourceSans3-Regular", 400, normal), 24 | ("Source Sans", "SourceSans3-RegularIt", 400, italic), 25 | ); 26 | 27 | // Map of font families. 28 | // 29 | // ``` 30 | // : (, ) 31 | // ``` 32 | $font-families: ( 33 | sans: join("Source Sans", $font-fallback-sans, $separator: comma), 34 | ); 35 | 36 | // Font directory 37 | $font-dir: "../fonts/"; 38 | 39 | // Functions 40 | // ========================================================================== 41 | 42 | // Imports the custom font. 43 | // 44 | // The mixin expects font files to be woff and woff2. 45 | // 46 | // @param {List} $webfont - A custom font to import, as a tuple: 47 | // ` `. 48 | // @param {String} $dir - The webfont directory path. 49 | // @output The `@font-face` at-rule specifying the custom font. 50 | 51 | @mixin font-face($webfont, $dir) { 52 | @font-face { 53 | font-display: swap; 54 | font-family: nth($webfont, 1); 55 | src: url("#{$dir}#{nth($webfont, 2)}.woff2") format("woff2"), 56 | url("#{$dir}#{nth($webfont, 2)}.woff") format("woff"); 57 | font-weight: #{nth($webfont, 3)}; 58 | font-style: #{nth($webfont, 4)}; 59 | } 60 | } 61 | 62 | // Imports the list of custom fonts. 63 | // 64 | // @require {mixin} font-face 65 | // 66 | // @param {List} $webfonts - List of custom fonts to import. 67 | // See `font-face` mixin for details. 68 | // @param {String} $dir - The webfont directory path. 69 | // @output The `@font-face` at-rules specifying the custom fonts. 70 | 71 | @mixin font-faces($webfonts, $dir) { 72 | @if (length($webfonts) > 0) { 73 | @if (type-of(nth($webfonts, 1)) == "list") { 74 | @each $webfont in $webfonts { 75 | @include font-face($webfont, $dir); 76 | } 77 | } @else { 78 | @include font-face($webfonts, $dir); 79 | } 80 | } 81 | } 82 | 83 | // Retrieves the font family stack for the given font ID. 84 | // 85 | // @require {variable} $font-families - See settings directory. 86 | // 87 | // @param {String} $font-family - The custom font ID. 88 | // @throws Error if the $font-family does not exist. 89 | // @return {List} The font stack. 90 | 91 | @function ff($font-family) { 92 | @if not map-has-key($font-families, $font-family) { 93 | @error "No font-family found in $font-families map for `#{$font-family}`."; 94 | } 95 | 96 | $value: map-get($font-families, $font-family); 97 | @return $value; 98 | } 99 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config 3 | // ========================================================================== 4 | 5 | // Context 6 | // ============================================================================= 7 | 8 | // The current stylesheet context. Available values: frontend, editor. 9 | $context: frontend !default; 10 | 11 | // Path is relative to the stylesheets directory. 12 | $assets-path: "../" !default; 13 | 14 | // Typography 15 | // ============================================================================= 16 | 17 | // Base 18 | $font-size: 16px; 19 | $line-height: math.div(24px, $font-size); 20 | $font-color: colorCode(darkest); 21 | 22 | // Weights 23 | $font-weight-light: 300; 24 | $font-weight-normal: 400; 25 | $font-weight-medium: 500; 26 | $font-weight-bold: 700; 27 | 28 | // Transition defaults 29 | // ============================================================================= 30 | $speed: t(normal); 31 | $easing: ease("power2.out"); 32 | 33 | // Spacing Units 34 | // ============================================================================= 35 | $unit: 60px; 36 | $unit-small: 20px; 37 | $vw-viewport: 1440; 38 | 39 | // Container 40 | // ========================================================================== 41 | $padding: $unit; 42 | 43 | // Grid 44 | // ========================================================================== 45 | $base-column-nb: 12; 46 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.spacings.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / Spacings 3 | // ========================================================================== 4 | 5 | :root { 6 | --spacing-2xs-mobile: 6; 7 | --spacing-2xs-desktop: 10; 8 | 9 | --spacing-xs-mobile: 12; 10 | --spacing-xs-desktop: 16; 11 | 12 | --spacing-sm-mobile: 22; 13 | --spacing-sm-desktop: 32; 14 | 15 | --spacing-md-mobile: 32; 16 | --spacing-md-desktop: 56; 17 | 18 | --spacing-lg-mobile: 48; 19 | --spacing-lg-desktop: 96; 20 | 21 | --spacing-xl-mobile: 64; 22 | --spacing-xl-desktop: 128; 23 | 24 | --spacing-2xl-mobile: 88; 25 | --spacing-2xl-desktop: 176; 26 | 27 | --spacing-3xl-mobile: 122; 28 | --spacing-3xl-desktop: 224; 29 | } 30 | 31 | // Spacings 32 | // ========================================================================== 33 | 34 | $spacings: ( 35 | 'gutter': var(--grid-gutter), 36 | '2xs': #{size-clamp('2xs')}, 37 | 'xs': #{size-clamp('xs')}, 38 | 'sm': #{size-clamp('sm')}, 39 | 'md': #{size-clamp('md')}, 40 | 'lg': #{size-clamp('lg')}, 41 | 'xl': #{size-clamp('xl')}, 42 | '2xl': #{size-clamp('2xl')}, 43 | '3xl': #{size-clamp('3xl')}, 44 | ); 45 | 46 | // Function 47 | // ========================================================================== 48 | 49 | // Returns spacing. 50 | // 51 | // ```scss 52 | // .c-box { 53 | // margin-top: spacing(gutter); 54 | // } 55 | // ``` 56 | // 57 | // @param {string} $key - The spacing key in $spacings. 58 | // @param {number} $multiplier - The multiplier of the spacing value. 59 | // @return {size} 60 | 61 | @function spacing($spacing: 'sm', $multiplier: 1) { 62 | @if not map-has-key($spacings, $spacing) { 63 | @error "Unknown master spacing: #{$spacing}"; 64 | } 65 | 66 | $index: map-get($spacings, $spacing); 67 | 68 | @return calc(#{$index} * #{$multiplier}); 69 | } 70 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.speeds.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / Speeds 3 | // ========================================================================== 4 | 5 | // Speeds 6 | // ========================================================================== 7 | 8 | $speeds: ( 9 | fastest: 0.1s, 10 | faster: 0.15s, 11 | fast: 0.25s, 12 | normal: 0.3s, 13 | slow: 0.5s, 14 | slower: 0.75s, 15 | slowest: 1s, 16 | ); 17 | 18 | // Function 19 | // ========================================================================== 20 | 21 | // Returns timing. 22 | // 23 | // ```scss 24 | // .c-box { 25 | // transition-duration: speed(slow); 26 | // } 27 | // ``` 28 | // 29 | // @param {string} $key - The speed key in $speeds. 30 | // @return {duration} 31 | 32 | @function speed($key: "normal") { 33 | @if not map-has-key($speeds, $key) { 34 | @error "Unknown '#{$key}' in $speeds."; 35 | } 36 | 37 | @return map-get($speeds, $key); 38 | } 39 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.variables.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / CSS VARS 3 | // ========================================================================== 4 | 5 | :root { 6 | 7 | // Grid 8 | --grid-columns: 4; 9 | --grid-gutter: #{rem(10px)}; 10 | --grid-margin: #{rem(10px)}; 11 | 12 | // Container 13 | --container-width: calc(100% - 2 * var(--grid-margin)); 14 | 15 | @media (min-width: $from-sm) { 16 | --grid-columns: #{$base-column-nb}; 17 | --grid-gutter: #{rem(16px)}; 18 | --grid-margin: #{rem(20px)}; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /assets/styles/settings/_config.zindexes.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Settings / Config / Z-indexes 3 | // ========================================================================== 4 | 5 | // Timings 6 | // ========================================================================== 7 | 8 | $z-indexes: ( 9 | "header": 200, 10 | "above": 1, 11 | "default": 0, 12 | "below": -1 13 | ); 14 | 15 | // Default z-index for z() 16 | $z-index-default: "above" !default; 17 | 18 | // Function 19 | // ========================================================================== 20 | 21 | // Retrieves the z-index from the {@see $layers master list}. 22 | // 23 | // @link on http://css-tricks.com/handling-z-index/ 24 | // 25 | // @param {string} $layer The name of the z-index. 26 | // @param {number} $modifier A positive or negative modifier to apply 27 | // to the returned z-index value. 28 | // @throw Error if the $layer does not exist. 29 | // @throw Warning if the $modifier might overlap another master z-index. 30 | // @return {number} The computed z-index of $layer and $modifier. 31 | 32 | @function z($layer: $z-index-default, $modifier: 0) { 33 | @if not map-has-key($z-indexes, $layer) { 34 | @error "Unknown master z-index layer: #{$layer}"; 35 | } 36 | 37 | @if ($modifier >= 50 or $modifier <= -50) { 38 | @warn "Modifier may overlap the another master z-index layer: #{$modifier}"; 39 | } 40 | 41 | $index: map-get($z-indexes, $layer); 42 | 43 | @return $index + $modifier; 44 | } 45 | -------------------------------------------------------------------------------- /assets/styles/tools/_functions.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Tools / Functions 3 | // ========================================================================== 4 | 5 | // Check if the given value is a number in pixel 6 | // 7 | // @param {Number} $number - The value to check 8 | // @return {Boolean} 9 | 10 | @function is-pixel-number($number) { 11 | @return type-of($number) == number and unit($number) == "px"; 12 | } 13 | 14 | // Converts the given pixel value to its EM quivalent. 15 | // 16 | // @param {Number} $size - The pixel value to convert. 17 | // @param {Number} $base [$font-size] - The assumed base font size. 18 | // @return {Number} Scalable pixel value in EMs. 19 | 20 | @function em($size, $base: $font-size) { 21 | @if not is-pixel-number($size) { 22 | @error "`#{$size}` needs to be a number in pixel."; 23 | } 24 | 25 | @if not is-pixel-number($base) { 26 | @error "`#{$base}` needs to be a number in pixel."; 27 | } 28 | 29 | @return math.div($size, $base) * 1em; 30 | } 31 | 32 | // Converts the given pixel value to its REM quivalent. 33 | // 34 | // @param {Number} $size - The pixel value to convert. 35 | // @param {Number} $base [$font-size] - The assumed base font size. 36 | // @return {Number} Scalable pixel value in REMs. 37 | 38 | @function rem($size, $base: $font-size) { 39 | 40 | @if not is-pixel-number($size) { 41 | @error "`#{$size}` needs to be a number in pixel."; 42 | } 43 | 44 | @if not is-pixel-number($base) { 45 | @error "`#{$base}` needs to be a number in pixel."; 46 | } 47 | 48 | @return math.div($size, $base) * 1rem; 49 | } 50 | 51 | // Checks if a list contains a value(s). 52 | // 53 | // @link https://github.com/thoughtbot/bourbon/blob/master/core/bourbon/validators/_contains.scss 54 | // @param {List} $list - The list to check against. 55 | // @param {List} $values - A single value or list of values to check for. 56 | // @return {Boolean} 57 | // @access private 58 | 59 | @function list-contains( 60 | $list, 61 | $values... 62 | ) { 63 | @each $value in $values { 64 | @if type-of(index($list, $value)) != "number" { 65 | @return false; 66 | } 67 | } 68 | 69 | @return true; 70 | } 71 | 72 | // Resolve whether a rule is important or not. 73 | // 74 | // @param {Boolean} $flag - Whether a rule is important (TRUE) or not (FALSE). 75 | // @return {String|Null} Returns `!important` or NULL. 76 | 77 | @function important($flag: false) { 78 | @if ($flag == true) { 79 | @return !important; 80 | } @else if ($important == false) { 81 | @return null; 82 | } @else { 83 | @error "`#{$flag}` needs to be `true` or `false`."; 84 | } 85 | } 86 | 87 | // Determine if the current context is for a WYSIWYG editor. 88 | // 89 | // @requires {String} $context - The global context of the stylesheet. 90 | // @return {Boolean} If the $context is set to "editor". 91 | 92 | @function is-editor() { 93 | @return ('editor' == $context); 94 | } 95 | 96 | // Determine if the current context is for the front-end. 97 | // 98 | // @requires {String} $context - The global context of the stylesheet. 99 | // @return {Boolean} If the $context is set to "frontend". 100 | 101 | @function is-frontend() { 102 | @return ('frontend' == $context); 103 | } 104 | 105 | $context: 'frontend' !default; 106 | 107 | // Returns calculation of a percentage of the grid cell width 108 | // with optional inset of grid gutter. 109 | // 110 | // ```scss 111 | // .c-box { 112 | // width: grid-space(6/12); 113 | // margin-left: grid-space(1/12, 1); 114 | // } 115 | // ``` 116 | // 117 | // @param {number} $percentage - The percentage spacer 118 | // @param {number} $inset - The grid gutter inset 119 | // @return {function} 120 | @function grid-space($percentage, $inset: 0) { 121 | @return calc(#{$percentage} * (#{vw(100)} - 2 * var(--grid-margin, 0px)) - (1 - #{$percentage}) * var(--grid-gutter, 0px) + #{$inset} * var(--grid-gutter, 0px)); 122 | } 123 | 124 | // Returns calculation of a percentage of the viewport small height. 125 | // 126 | // ```scss 127 | // .c-box { 128 | // height: svh(100); 129 | // } 130 | // ``` 131 | // 132 | // @param {number} $number - The percentage number 133 | // @return {function} in svh 134 | @function svh($number) { 135 | @return calc(#{$number} * var(--svh, 1svh)); 136 | } 137 | 138 | // Returns calculation of a percentage of the viewport large height. 139 | // 140 | // ```scss 141 | // .c-box { 142 | // height: lvh(100); 143 | // } 144 | // ``` 145 | // 146 | // @param {number} $number - The percentage number 147 | // @return {function} in lvh 148 | @function lvh($number) { 149 | @return calc(#{$number} * var(--lvh, 1lvh)); 150 | } 151 | 152 | // Returns calculation of a percentage of the viewport dynamic height. 153 | // 154 | // ```scss 155 | // .c-box { 156 | // height: dvh(100); 157 | // } 158 | // ``` 159 | // 160 | // @param {number} $number - The percentage number 161 | // @return {function} in dvh 162 | @function dvh($number) { 163 | @return calc(#{$number} * var(--dvh, 1dvh)); 164 | } 165 | 166 | // Returns calculation of a percentage of the viewport width. 167 | // 168 | // ```scss 169 | // .c-box { 170 | // width: vw(100); 171 | // } 172 | // ``` 173 | // 174 | // @param {number} $number - The percentage number 175 | // @return {function} in vw 176 | 177 | @function vw($number) { 178 | @return calc(#{$number} * var(--vw, 1vw)); 179 | } 180 | 181 | @function clamp-with-max($min, $size, $max) { 182 | $vw-context: $vw-viewport * 0.01; 183 | @return clamp(#{$min}, calc(#{$size} / #{$vw-context} * 1vw), #{$max}); 184 | } 185 | 186 | @function size-clamp($size) { 187 | @return clamp-with-max( 188 | calc(#{rem(1px)} * var(--spacing-#{$size}-mobile)), 189 | var(--spacing-#{$size}-desktop), 190 | calc(#{rem(1px)} * var(--spacing-#{$size}-desktop)) 191 | ); 192 | } 193 | 194 | // Returns clamp of calculated preferred responsive font size 195 | // within a font size and breakpoint range. 196 | // 197 | // ```scss 198 | // .c-heading.-h1 { 199 | // font-size: responsive-value(30px, 60px, 1800); 200 | // } 201 | // 202 | // .c-heading.-h2 { 203 | // font-size: responsive-value(20px, 40px, $from-xl); 204 | // } 205 | // ``` 206 | // 207 | // @param {number} $min-size - Minimum font size in pixels. 208 | // @param {number} $max-size - Maximum font size in pixels. 209 | // @param {number} $breakpoint - Maximum breakpoint. 210 | // @return {function, number>} 211 | @function responsive-value($min-size, $max-size, $breakpoint) { 212 | $delta: math.div($max-size, $breakpoint); 213 | @return clamp($min-size, calc(#{strip-unit($delta)} * #{vw(100)}), $max-size); 214 | } 215 | -------------------------------------------------------------------------------- /assets/styles/tools/_layout.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Tools / Layout 3 | // ========================================================================== 4 | 5 | // Grid-like layout system. 6 | // 7 | // The layout tools provide a column-style layout system. This file contains 8 | // the mixins to generate basic structural elements. 9 | // 10 | // @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss 11 | // 12 | // 13 | // Generate the layout container. 14 | // 15 | // 1. Use the negative margin trick for multi-row grids: 16 | // http://csswizardry.com/2011/08/building-better-grid-systems/ 17 | // 18 | // @requires {function} u-list-reset 19 | // @output `font-size`, `margin`, `padding`, `list-style` 20 | 21 | @mixin o-layout($gutter: 0, $fix-whitespace: true) { 22 | margin: 0; 23 | padding: 0; 24 | list-style: none; 25 | 26 | @if ($fix-whitespace) { 27 | font-size: 0; 28 | } 29 | 30 | @if (type-of($gutter) == number) { 31 | margin-left: -$gutter; // [1] 32 | } 33 | } 34 | 35 | // Generate the layout item. 36 | // 37 | // 1. Required in order to combine fluid widths with fixed gutters. 38 | // 2. Allows us to manipulate grids vertically, with text-level properties, 39 | // etc. 40 | // 3. Default item alignment is with the tops of each other, like most 41 | // traditional grid/layout systems. 42 | // 4. By default, all layout items are full-width (mobile first). 43 | // 5. Gutters provided by left padding: 44 | // http://csswizardry.com/2011/08/building-better-grid-systems/ 45 | 46 | @mixin o-layout_item($gutter: 0, $fix-whitespace: true) { 47 | display: inline-block; // [2] 48 | width: 100%; // [4] 49 | vertical-align: top; // [3] 50 | 51 | @if ($fix-whitespace) { 52 | font-size: 1rem; 53 | } 54 | 55 | @if (type-of($gutter) == number) { 56 | padding-left: $gutter; // [5] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /assets/styles/tools/_maths.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Tools / Maths 3 | // ========================================================================== 4 | 5 | // Remove the unit of a length 6 | // 7 | // @param {Number} $number Number to remove unit from 8 | // @return {function} 9 | @function strip-unit($value) { 10 | @if type-of($value) != "number" { 11 | @error "Invalid `#{type-of($value)}` type. Choose a number type instead."; 12 | } @else if type-of($value) == "number" and not is-unitless($value) { 13 | @return math.div($value, $value * 0 + 1); 14 | } 15 | 16 | @return $value; 17 | } 18 | 19 | // Returns the square root of the given number. 20 | // 21 | // @param {number} $number The number to calculate. 22 | // @return {number} 23 | 24 | @function sqrt($number) { 25 | $x: 1; 26 | $value: $x; 27 | 28 | @for $i from 1 through 10 { 29 | $value: $x - math.div(($x * $x - abs($number)), (2 * $x)); 30 | $x: $value; 31 | } 32 | 33 | @return $value; 34 | } 35 | 36 | // Returns a number raised to the power of an exponent. 37 | // 38 | // @param {number} $number The base number. 39 | // @param {number} $exp The exponent. 40 | // @return {number} 41 | 42 | @function pow($number, $exp) { 43 | $value: 1; 44 | 45 | @if $exp > 0 { 46 | @for $i from 1 through $exp { 47 | $value: $value * $number; 48 | } 49 | } @else if $exp < 0 { 50 | @for $i from 1 through -$exp { 51 | $value: math.div($value, $number); 52 | } 53 | } 54 | 55 | @return $value; 56 | } 57 | 58 | // Returns the factorial of the given number. 59 | // 60 | // @param {number} $number The number to calculate. 61 | // @return {number} 62 | 63 | @function fact($number) { 64 | $value: 1; 65 | 66 | @if $number > 0 { 67 | @for $i from 1 through $number { 68 | $value: $value * $i; 69 | } 70 | } 71 | 72 | @return $value; 73 | } 74 | 75 | // Returns an approximation of pi, with 11 decimals. 76 | // 77 | // @return {number} 78 | 79 | @function pi() { 80 | @return 3.14159265359; 81 | } 82 | 83 | // Converts the number in degrees to the radian equivalent . 84 | // 85 | // @param {number} $angle The angular value to calculate. 86 | // @return {number} If $angle has the `deg` unit, 87 | // the radian equivalent is returned. 88 | // Otherwise, the unitless value of $angle is returned. 89 | 90 | @function rad($angle) { 91 | $unit: unit($angle); 92 | $angle: strip-units($angle); 93 | 94 | // If the angle has `deg` as unit, convert to radians. 95 | @if ($unit == deg) { 96 | @return math.div($angle, 180) * pi(); 97 | } 98 | 99 | @return $angle; 100 | } 101 | 102 | // Returns the sine of the given number. 103 | // 104 | // @param {number} $angle The angle to calculate. 105 | // @return {number} 106 | 107 | @function sin($angle) { 108 | $sin: 0; 109 | $angle: rad($angle); 110 | 111 | @for $i from 0 through 10 { 112 | $sin: $sin + pow(-1, $i) * math.div(pow($angle, (2 * $i + 1)), fact(2 * $i + 1)); 113 | } 114 | 115 | @return $sin; 116 | } 117 | 118 | // Returns the cosine of the given number. 119 | // 120 | // @param {string} $angle The angle to calculate. 121 | // @return {number} 122 | 123 | @function cos($angle) { 124 | $cos: 0; 125 | $angle: rad($angle); 126 | 127 | @for $i from 0 through 10 { 128 | $cos: $cos + pow(-1, $i) * math.div(pow($angle, 2 * $i), fact(2 * $i)); 129 | } 130 | 131 | @return $cos; 132 | } 133 | 134 | // Returns the tangent of the given number. 135 | // 136 | // @param {string} $angle The angle to calculate. 137 | // @return {number} 138 | 139 | @function tan($angle) { 140 | @return math.div(sin($angle), cos($angle)); 141 | } 142 | -------------------------------------------------------------------------------- /assets/styles/tools/_mixins.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Tools / Mixins 3 | // ========================================================================== 4 | 5 | // Set the color of the highlight that appears over a link while it's being tapped. 6 | // 7 | // By default, the highlight is suppressed. 8 | // 9 | // @param {Color} $value [rgba(0, 0, 0, 0)] - The value of the highlight. 10 | // @output `-webkit-tap-highlight-color` 11 | 12 | @mixin tap-highlight-color($value: rgba(0, 0, 0, 0)) { 13 | -webkit-tap-highlight-color: $value; 14 | } 15 | 16 | // Set whether or not touch devices use momentum-based scrolling for the given element. 17 | // 18 | // By default, applies momentum-based scrolling for the current element. 19 | // 20 | // @param {String} $value [rgba(0, 0, 0, 0)] - The type of scrolling. 21 | // @output `-webkit-overflow-scrolling` 22 | 23 | @mixin overflow-scrolling($value: touch) { 24 | -webkit-overflow-scrolling: $value; 25 | } 26 | 27 | // Micro clearfix rules for containing floats. 28 | // 29 | // @link http://www.cssmojo.com/the-very-latest-clearfix-reloaded/ 30 | // @param {List} $supports The type of clearfix to generate. 31 | // @output Injects `:::after` pseudo-element. 32 | 33 | @mixin u-clearfix($supports...) { 34 | &::after { 35 | display: if(list-contains($supports, table), table, block); 36 | clear: both; 37 | content: if(list-contains($supports, opera), " ", ""); 38 | } 39 | } 40 | 41 | // Generate a font-size and baseline-compatible line-height. 42 | // 43 | // @link https://github.com/inuitcss/inuitcss/c14029c/tools/_tools.font-size.scss 44 | // @param {Number} $font-size - The size of the font. 45 | // @param {Number} $line-height [auto] - The line box height. 46 | // @param {Boolean} $important [false] - Whether the font-size is important. 47 | // @output `font-size`, `line-height` 48 | 49 | @mixin font-size($font-size, $line-height: auto, $important: false) { 50 | $important: important($important); 51 | font-size: rem($font-size) $important; 52 | 53 | @if ($line-height == "auto") { 54 | line-height: ceil(math.div($font-size, $line-height)) * math.div($line-height, $font-size) $important; 55 | } 56 | @else { 57 | @if (type-of($line-height) == number or $line-height == "inherit" or $line-height == "normal") { 58 | line-height: $line-height $important; 59 | } 60 | @else if ($line-height != "none" and $line-height != false) { 61 | @error "D’oh! `#{$line-height}` is not a valid value for `$line-height`."; 62 | } 63 | } 64 | } 65 | 66 | // Vertically-center the direct descendants of the current element. 67 | // 68 | // Centering is achieved by displaying children as inline-blocks. Any whitespace 69 | // between elements is nullified by redefining the font size of the container 70 | // and its children. 71 | // 72 | // @output `font-size`, `display`, `vertical-align` 73 | 74 | @mixin o-vertical-center { 75 | font-size: 0; 76 | 77 | &::before { 78 | display: inline-block; 79 | height: 100%; 80 | content: ""; 81 | vertical-align: middle; 82 | } 83 | 84 | > * { 85 | display: inline-block; 86 | vertical-align: middle; 87 | font-size: 1rem; 88 | } 89 | } 90 | 91 | // Generate `:hover` and `:focus` styles in one go. 92 | // 93 | // @link https://github.com/inuitcss/inuitcss/blob/master/tools/_tools.mixins.scss 94 | // @content Wrapped in `:focus` and `:hover` pseudo-classes. 95 | // @output Wraps the given content in `:focus` and `:hover` pseudo-classes. 96 | 97 | @mixin u-hocus { 98 | &:focus, 99 | &:hover { 100 | @content; 101 | } 102 | } 103 | 104 | // Generate `:active` and `:focus` styles in one go. 105 | // 106 | // @see {Mixin} u-hocus 107 | // @content Wrapped in `:focus` and `:active` pseudo-classes. 108 | // @output Wraps the given content in `:focus` and `:hover` pseudo-classes. 109 | 110 | @mixin u-actus { 111 | &:focus, 112 | &:active { 113 | @content; 114 | } 115 | } 116 | 117 | // Prevent text from wrapping onto multiple lines for the current element. 118 | // 119 | // An ellipsis is appended to the end of the line. 120 | // 121 | // 1. Ensure that the node has a maximum width after which truncation can occur. 122 | // 2. Fix for IE 8/9 if `word-wrap: break-word` is in effect on ancestor nodes. 123 | // 124 | // @param {Number} $width [100%] - The maximum width of element. 125 | // @output `max-width`, `word-wrap`, `white-space`, `overflow`, `text-overflow` 126 | 127 | @mixin u-truncate($width: 100%) { 128 | overflow: hidden; 129 | text-overflow: ellipsis; 130 | white-space: nowrap; 131 | word-wrap: normal; // [2] 132 | @if $width { 133 | max-width: $width; // [1] 134 | } 135 | } 136 | 137 | // Applies accessible hiding to the current element. 138 | // 139 | // @param {Boolean} $important [true] - Whether the visibility is important. 140 | // @output Properties for removing the element from the document flow. 141 | 142 | @mixin u-accessibly-hidden($important: true) { 143 | $important: important($important); 144 | position: absolute $important; 145 | overflow: hidden; 146 | clip: rect(0 0 0 0); 147 | margin: 0; 148 | padding: 0; 149 | width: 1px; 150 | height: 1px; 151 | border: 0; 152 | } 153 | 154 | // Allows an accessibly hidden element to be focusable via keyboard navigation. 155 | // 156 | // @content For styling the now visible element. 157 | // @output Injects `:focus`, `:active` pseudo-classes. 158 | 159 | @mixin u-accessibly-focusable { 160 | @include u-actus { 161 | clip: auto; 162 | width: auto; 163 | height: auto; 164 | 165 | @content; 166 | } 167 | } 168 | 169 | // Hide the current element from all. 170 | // 171 | // The element will be hidden from screen readers and removed from the document flow. 172 | // 173 | // @link http://juicystudio.com/article/screen-readers-display-none.php 174 | // @param {Boolean} $important [true] - Whether the visibility is important. 175 | // @output `display`, `visibility` 176 | 177 | @mixin u-hidden($important: true) { 178 | $important: important($important); 179 | display: none $important; 180 | visibility: hidden $important; 181 | } 182 | 183 | // Show the current element for all. 184 | // 185 | // The element will be accessible from screen readers and visible in the document flow. 186 | // 187 | // @param {String} $display [block] - The rendering box used for the element. 188 | // @param {Boolean} $important [true] - Whether the visibility is important. 189 | // @output `display`, `visibility` 190 | 191 | @mixin u-shown($display: block, $important: true) { 192 | $important: important($important); 193 | display: $display $important; 194 | visibility: visible $important; 195 | } 196 | 197 | // Aspect-ratio polyfill 198 | // 199 | // @param {Number} $ratio [19/6] - The ratio of the element. 200 | // @param {Number} $width [100%] - The fallback width of element. 201 | // @param {Boolean} $children [false] - Whether the element contains children for the fallback properties. 202 | // @output Properties for maintaining aspect-ratio 203 | 204 | @mixin aspect-ratio($ratio: math.div(16, 9), $width: 100%, $children: false) { 205 | 206 | @supports (aspect-ratio: 1) { 207 | aspect-ratio: $ratio; 208 | } 209 | 210 | @supports not (aspect-ratio: 1) { 211 | height: 0; 212 | padding-top: calc(#{$width} * #{math.div(1, $ratio)}); 213 | 214 | @if ($children == true) { 215 | position: relative; 216 | 217 | > * { 218 | position: absolute; 219 | top: 0; 220 | left: 0; 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /assets/styles/tools/_widths.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Tools / Widths 3 | // ========================================================================== 4 | 5 | // Optionally, the boilerplate can generate classes to offset items by a 6 | // certain width. Would you like to generate these types of class as well? E.g.: 7 | // 8 | // @example css 9 | // .u-push-1/3 10 | // .u-pull-2/4 11 | // .u-pull-1/5 12 | // .u-push-2/3 13 | 14 | $widths-offsets: false !default; 15 | 16 | // By default, the boilerplate uses fractions-like classes like `
`. 17 | // You can change the `/` to whatever you fancy with this variable. 18 | 19 | $fractions-delimiter: \/ !default; 20 | 21 | // When using Sass-MQ, this defines the separator for the breakpoints suffix 22 | // in the class name. By default, we are generating the responsive suffixes 23 | // for the classes with a `@` symbol so you get classes like: 24 | //
25 | 26 | $breakpoint-delimiter: \@ !default; 27 | 28 | // Generate a series of width helper classes 29 | // 30 | // @example scss 31 | // @include widths(12); 32 | // 33 | // @example html 34 | //
35 | // 36 | // @example scss 37 | // @include widths(3 4, -mobile); 38 | // 39 | // @example html 40 | //
41 | // 42 | // @link https://github.com/inuitcss/inuitcss/commit/6eb574f/utilities/_utilities.widths.scss 43 | // @requires {Function} important 44 | // @requires {Function} $widths-offsets 45 | // @requires {Function} $fractions-delimiter 46 | // @requires {Function} $breakpoint-delimiter 47 | // @param {List} $colums - The columns we want the widths to have. 48 | // @param {String} $breakpoint - Optional suffix for responsive widths. 49 | // @output `width`, `position`, `right`, `left` 50 | 51 | @mixin widths($columns, $breakpoint: null, $important: true) { 52 | $important: important($important); 53 | 54 | // Loop through the number of columns for each denominator of our fractions. 55 | @each $denominator in $columns { 56 | // Begin creating a numerator for our fraction up until we hit the 57 | // denominator. 58 | @for $numerator from 1 through $denominator { 59 | // Build a class in the format `.u-3/4[@]`. 60 | .u-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} { 61 | width: math.div($numerator, $denominator) * 100% $important; 62 | } 63 | 64 | @if ($widths-offsets == true) { 65 | // Build a class in the format `.u-push-1/2[@]`. 66 | .u-push-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} { 67 | position: relative $important; 68 | right: auto $important; 69 | left: math.div($numerator, $denominator) * 100% $important; 70 | } 71 | 72 | // Build a class in the format `.u-pull-5/6[@]`. 73 | .u-pull-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} { 74 | position: relative $important; 75 | right: math.div($numerator, $denominator) * 100% $important; 76 | left: auto $important; 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /assets/styles/utilities/_align.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / Alignment 3 | // ========================================================================== 4 | 5 | // Floats 6 | // ========================================================================== 7 | 8 | .u-float-left { 9 | float: left !important; 10 | } 11 | 12 | .u-float-right { 13 | float: right !important; 14 | } 15 | 16 | // Horizontal Text 17 | // ========================================================================== 18 | 19 | .u-text-center { 20 | text-align: center !important; 21 | } 22 | 23 | .u-text-left { 24 | text-align: left !important; 25 | } 26 | 27 | .u-text-right { 28 | text-align: right !important; 29 | } 30 | 31 | // Vertical Text 32 | // ========================================================================== 33 | 34 | .u-align-baseline { 35 | vertical-align: baseline !important; 36 | } 37 | 38 | .u-align-bottom { 39 | vertical-align: bottom !important; 40 | } 41 | 42 | .u-align-middle { 43 | vertical-align: middle !important; 44 | } 45 | 46 | .u-align-top { 47 | vertical-align: top !important; 48 | } 49 | 50 | .u-vertical-center { 51 | @include o-vertical-center; 52 | } 53 | -------------------------------------------------------------------------------- /assets/styles/utilities/_grid-column.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Tools / Grid Columns 3 | // ========================================================================== 4 | 5 | // 6 | // Grid layout system. 7 | // 8 | // This tool generates columns for all needed media queries. 9 | // Unused classes will be purge by the css post-processor. 10 | // 11 | 12 | $colsMax: $base-column-nb + 1; 13 | 14 | @each $breakpoint, $mediaquery in $breakpoints { 15 | @for $fromIndex from 1 through $colsMax { 16 | @for $toIndex from 1 through $colsMax { 17 | 18 | // Columns without media query 19 | @if $breakpoint == "tiny" { 20 | .u-gc-#{$fromIndex}\/#{$toIndex} { 21 | --gc-start: #{$fromIndex}; 22 | --gc-end: #{$toIndex}; 23 | } 24 | } 25 | 26 | // Columns min-width breakpoints `@from-*` 27 | .u-gc-#{$fromIndex}\/#{$toIndex}\@from-#{$breakpoint} { 28 | @media #{mq-min($breakpoint)} { 29 | --gc-start: #{$fromIndex}; 30 | --gc-end: #{$toIndex}; 31 | } 32 | } 33 | 34 | // Columns max-width breakpoints @to-*` 35 | .u-gc-#{$fromIndex}\/#{$toIndex}\@to-#{$breakpoint} { 36 | @media #{mq-max($breakpoint)} { 37 | --gc-start: #{$fromIndex}; 38 | --gc-end: #{$toIndex}; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/styles/utilities/_helpers.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / Helpers 3 | // ========================================================================== 4 | 5 | // Layout 6 | // ========================================================================== 7 | 8 | .u-clearfix { 9 | @include u-clearfix; 10 | } 11 | 12 | // Decorative 13 | // ============================================================================= 14 | 15 | .u-truncate { 16 | @include u-truncate; 17 | } 18 | 19 | // Visibility / Display 20 | // ========================================================================== 21 | 22 | [hidden][aria-hidden="false"] { 23 | position: absolute; 24 | display: inherit; 25 | clip: rect(0, 0, 0, 0); 26 | } 27 | 28 | [hidden][aria-hidden="false"]:focus { 29 | clip: auto; 30 | } 31 | 32 | // .u-block { 33 | // display: block; 34 | // } 35 | 36 | // // 1. Fix for Firefox bug: an image styled `max-width:100%` within an 37 | // // inline-block will display at its default size, and not limit its width to 38 | // // 100% of an ancestral container. 39 | // 40 | // .u-inline-block { 41 | // display: inline-block !important; 42 | // max-width: 100%; /* 1 */ 43 | // } 44 | // 45 | // .u-inline { 46 | // display: inline !important; 47 | // } 48 | // 49 | // .u-table { 50 | // display: table !important; 51 | // } 52 | // 53 | // .u-tableCell { 54 | // display: table-cell !important; 55 | // } 56 | // 57 | // .u-tableRow { 58 | // display: table-row !important; 59 | // } 60 | 61 | // Completely remove from the flow but leave available to screen readers. 62 | 63 | .u-screen-reader-text { 64 | @include u-accessibly-hidden; 65 | } 66 | 67 | @media not print { 68 | .u-screen-reader-text\@screen { 69 | @include u-accessibly-hidden; 70 | } 71 | } 72 | 73 | // Extends the `.screen-reader-text` class to allow the element 74 | // to be focusable when navigated to via the keyboard. 75 | // 76 | // @link https://www.drupal.org/node/897638 77 | // @todo Define styles when focused. 78 | 79 | .u-screen-reader-text.-focusable { 80 | @include u-accessibly-focusable; 81 | } 82 | -------------------------------------------------------------------------------- /assets/styles/utilities/_print.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / Print Mode 3 | // ========================================================================== 4 | 5 | //// 6 | /// Very crude, reset-like styles taken from the HTML5 Boilerplate: 7 | /// - https://github.com/h5bp/html5-boilerplate/blob/5.3.0/dist/doc/css.md#print-styles 8 | /// - https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css#L205-L282 9 | /// 10 | /// @link https://github.com/inuitcss/inuitcss/blob/c27993f/utilities/_utilities.print.scss 11 | //// 12 | 13 | @media print { 14 | 15 | // 1. Black prints faster: http://www.sanbeiji.com/archives/953 16 | 17 | *, 18 | *:before, 19 | *:after, 20 | *:first-letter, 21 | *:first-line { 22 | background: transparent !important; 23 | box-shadow: none !important; 24 | color: #000000 !important; // [1] 25 | text-shadow: none !important; 26 | } 27 | 28 | a, 29 | a:visited { 30 | text-decoration: underline; 31 | } 32 | 33 | a[href]:after { 34 | content: " (" attr(href) ")"; 35 | } 36 | 37 | abbr[title]:after { 38 | content: " (" attr(title) ")"; 39 | } 40 | 41 | // Don't show links that are fragment identifiers, or use the `javascript:` 42 | // pseudo protocol. 43 | 44 | a[href^="#"]:after, 45 | a[href^="javascript:"]:after { 46 | content: ""; 47 | } 48 | 49 | pre, 50 | blockquote { 51 | border: 1px solid #999999; 52 | page-break-inside: avoid; 53 | } 54 | 55 | // Printing Tables: http://css-discuss.incutio.com/wiki/Printing_Tables 56 | 57 | thead { 58 | display: table-header-group; 59 | } 60 | 61 | tr, 62 | img { 63 | page-break-inside: avoid; 64 | } 65 | 66 | 67 | img { 68 | max-width: 100% !important; 69 | } 70 | 71 | p, 72 | h2, 73 | h3 { 74 | orphans: 3; 75 | widows: 3; 76 | } 77 | 78 | h2, 79 | h3 { 80 | page-break-after: avoid; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /assets/styles/utilities/_ratio.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / Ratio 3 | // ========================================================================== 4 | 5 | // @link https://github.com/inuitcss/inuitcss/blob/19d0c7e/objects/_objects.ratio.scss 6 | 7 | // A list of aspect ratios that get generated as modifier classes. 8 | $aspect-ratios: ( 9 | (2:1), 10 | (4:3), 11 | (16:9), 12 | ) !default; 13 | 14 | /* stylelint-disable */ 15 | 16 | // Generate a series of ratio classes to be used like so: 17 | // 18 | // @example 19 | //
20 | 21 | @each $ratio in $aspect-ratios { 22 | @each $antecedent, $consequent in $ratio { 23 | @if (type-of($antecedent) != number) { 24 | @error "`#{$antecedent}` needs to be a number." 25 | } 26 | 27 | @if (type-of($consequent) != number) { 28 | @error "`#{$consequent}` needs to be a number." 29 | } 30 | 31 | .u-#{$antecedent}\:#{$consequent}::before { 32 | padding-bottom: math.div($consequent, $antecedent) * 100%; 33 | } 34 | } 35 | } 36 | 37 | /* stylelint-enable */ 38 | -------------------------------------------------------------------------------- /assets/styles/utilities/_spacing.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / Spacing 3 | // ========================================================================== 4 | 5 | //// 6 | /// Utility classes to put specific spacing values onto elements. The below loop 7 | /// will generate us a suite of classes like: 8 | /// 9 | /// @example 10 | /// .u-margin-top {} 11 | /// .u-margin-top-xs {} 12 | /// .u-padding-left-lg {} 13 | /// .u-margin-right-sm {} 14 | /// .u-padding {} 15 | /// .u-padding-right-none {} 16 | /// 17 | /// @link https://github.com/inuitcss/inuitcss/blob/512977a/utilities/_utilities.spacing.scss 18 | //// 19 | 20 | /* stylelint-disable string-quotes */ 21 | 22 | $spacing-directions: ( 23 | null: null, 24 | '-top': '-top', 25 | '-right': '-right', 26 | '-bottom': '-bottom', 27 | '-left': '-left', 28 | '-x': '-left' '-right', 29 | '-y': '-top' '-bottom', 30 | ) !default; 31 | 32 | $spacing-properties: ( 33 | 'padding': 'padding', 34 | 'margin': 'margin', 35 | ) !default; 36 | 37 | $spacing-sizes: join($spacings, ( 38 | null: var(--grid-gutter), 39 | 'none': 0 40 | )); 41 | 42 | @each $breakpoint, $mediaquery in $breakpoints { 43 | @each $property-namespace, $property in $spacing-properties { 44 | @each $direction-namespace, $directions in $spacing-directions { 45 | @each $size-namespace, $size in $spacing-sizes { 46 | 47 | // Prepend "-" to spacing sizes if not null 48 | $size-namespace: if($size-namespace != null, "-" + $size-namespace, $size-namespace); 49 | 50 | // Base class 51 | $base-class: ".u-" + #{$property-namespace}#{$direction-namespace}#{$size-namespace}; 52 | 53 | // Spacing without media query 54 | @if $breakpoint == "xs" { 55 | #{$base-class} { 56 | @each $direction in $directions { 57 | #{$property}#{$direction}: $size !important; 58 | } 59 | } 60 | } 61 | 62 | // Spacing min-width breakpoints `@from-*` 63 | #{$base-class}\@from-#{$breakpoint} { 64 | @media #{mq-min($breakpoint)} { 65 | @each $direction in $directions { 66 | #{$property}#{$direction}: $size !important; 67 | } 68 | } 69 | } 70 | 71 | // Spacing max-width breakpoints @to-*` 72 | #{$base-class}\@to-#{$breakpoint} { 73 | @media #{mq-max($breakpoint)} { 74 | @each $direction in $directions { 75 | #{$property}#{$direction}: $size !important; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | /* stylelint-enable string-quotes */ 85 | -------------------------------------------------------------------------------- /assets/styles/utilities/_states.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / States 3 | // ========================================================================== 4 | 5 | // ARIA roles display visual cursor hints 6 | 7 | [aria-busy="true"] { 8 | cursor: progress; 9 | } 10 | 11 | [aria-controls] { 12 | cursor: pointer; 13 | } 14 | 15 | [aria-disabled] { 16 | cursor: default; 17 | } 18 | 19 | // Control visibility without affecting flow. 20 | 21 | .is-visible { 22 | visibility: visible !important; 23 | opacity: 1 !important; 24 | } 25 | 26 | .is-invisible { 27 | visibility: hidden !important; 28 | opacity: 0 !important; 29 | } 30 | 31 | // Completely remove from the flow and screen readers. 32 | 33 | .is-hidden { 34 | @include u-hidden; 35 | } 36 | 37 | @media not print { 38 | .is-hidden\@screen { 39 | @include u-hidden; 40 | } 41 | } 42 | 43 | @media print { 44 | .is-hidden\@print { 45 | @include u-hidden; 46 | } 47 | } 48 | 49 | // .is-hidden\@to-lg { 50 | // @media (max-width: $to-lg) { 51 | // display: none; 52 | // } 53 | // } 54 | // 55 | // .is-hidden\@from-lg { 56 | // @media (min-width: $from-lg) { 57 | // display: none; 58 | // } 59 | // } 60 | 61 | // // Display a hidden-by-default element. 62 | // 63 | // .is-shown { 64 | // @include u-shown; 65 | // } 66 | // 67 | // table.is-shown { 68 | // display: table !important; 69 | // } 70 | // 71 | // tr.is-shown { 72 | // display: table-row !important; 73 | // } 74 | // 75 | // td.is-shown, 76 | // th.is-shown { 77 | // display: table-cell !important; 78 | // } 79 | -------------------------------------------------------------------------------- /assets/styles/utilities/_widths.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Utilities / Widths 3 | // ========================================================================== 4 | 5 | //// 6 | /// @link https://github.com/inuitcss/inuitcss/blob/6eb574f/utilities/_utilities.widths.scss 7 | /// 8 | /// 9 | /// Which fractions would you like in your grid system(s)? 10 | /// By default, the boilerplate provides fractions of one whole, halves, thirds, 11 | /// quarters, and fifths, e.g.: 12 | /// 13 | /// @example css 14 | /// .u-1/2 15 | /// .u-2/5 16 | /// .u-3/4 17 | /// .u-2/3 18 | //// 19 | 20 | $widths-fractions: 1 2 3 4 5 !default; 21 | 22 | @include widths($widths-fractions); 23 | 24 | .u-1\/2\@from-sm { 25 | @media (min-width: $from-sm) { 26 | width: 50%; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/styles/vendors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/assets/styles/vendors/.gitkeep -------------------------------------------------------------------------------- /assets/svgs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/assets/svgs/.gitkeep -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | import concatFiles from './tasks/concats.js'; 2 | import compileScripts from './tasks/scripts.js'; 3 | import compileStyles from './tasks/styles.js'; 4 | import compileSVGs from './tasks/svgs.js'; 5 | import bumpVersions from './tasks/versions.js'; 6 | 7 | concatFiles(); 8 | compileScripts(); 9 | compileStyles(); 10 | compileSVGs(); 11 | bumpVersions(); 12 | -------------------------------------------------------------------------------- /build/helpers/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Provides simple user configuration options. 3 | */ 4 | 5 | import loconfig from '../../loconfig.json' with { type: 'json' }; 6 | import { merge } from '../utils/index.js'; 7 | 8 | let usrconfig; 9 | 10 | try { 11 | usrconfig = await import('../../loconfig.local.json', { 12 | with: { type: 'json' }, 13 | }); 14 | usrconfig = usrconfig.default; 15 | 16 | merge(loconfig, usrconfig); 17 | } catch (err) { 18 | // do nothing 19 | } 20 | 21 | export default loconfig; 22 | 23 | export { 24 | loconfig, 25 | }; 26 | -------------------------------------------------------------------------------- /build/helpers/glob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Retrieve the first available glob library. 3 | * 4 | * Note that options vary between libraries. 5 | * 6 | * Candidates: 7 | * 8 | * - {@link https://npmjs.com/package/tiny-glob tiny-glob} [1][5][6] 9 | * - {@link https://npmjs.com/package/globby globby} [2][5] 10 | * - {@link https://npmjs.com/package/fast-glob fast-glob} [3] 11 | * - {@link https://npmjs.com/package/glob glob} [1][4][5] 12 | * 13 | * Notes: 14 | * 15 | * - [1] The library's function accepts only a single pattern. 16 | * - [2] The library's function accepts only an array of patterns. 17 | * - [3] The library's function accepts either a single pattern 18 | * or an array of patterns. 19 | * - [4] The library's function does not return a Promise but will be 20 | * wrapped in a function that does return a Promise. 21 | * - [5] The library's function will be wrapped in a function that 22 | * supports a single pattern and an array of patterns. 23 | * - [6] The library's function returns files and directories but will be 24 | * preconfigured to return only files. 25 | */ 26 | 27 | import { promisify } from 'node:util'; 28 | 29 | /** 30 | * @callback GlobFn 31 | * 32 | * @param {string|string[]} patterns - A string pattern 33 | * or an array of string patterns. 34 | * @param {object} options 35 | * 36 | * @returns {Promise} 37 | */ 38 | 39 | /** 40 | * @typedef {object} GlobOptions 41 | */ 42 | 43 | /** 44 | * @type {GlobFn|undefined} The discovered glob function. 45 | */ 46 | let glob; 47 | 48 | /** 49 | * @type {string[]} A list of packages to attempt import. 50 | */ 51 | const candidates = [ 52 | 'tiny-glob', 53 | 'globby', 54 | 'fast-glob', 55 | 'glob', 56 | ]; 57 | 58 | try { 59 | glob = await importGlob(); 60 | } catch (err) { 61 | // do nothing 62 | } 63 | 64 | /** 65 | * @type {boolean} Whether a glob function was discovered (TRUE) or not (FALSE). 66 | */ 67 | const supportsGlob = (typeof glob === 'function'); 68 | 69 | /** 70 | * Imports the first available glob function. 71 | * 72 | * @throws {TypeError} If no glob library was found. 73 | * 74 | * @returns {GlobFn} 75 | */ 76 | async function importGlob() { 77 | for (let name of candidates) { 78 | try { 79 | let globModule = await import(name); 80 | 81 | if (typeof globModule.default !== 'function') { 82 | throw new TypeError(`Expected ${name} to be a function`); 83 | } 84 | 85 | /** 86 | * Wrap the function to ensure 87 | * a common pattern. 88 | */ 89 | switch (name) { 90 | case 'tiny-glob': 91 | /** [1][5] */ 92 | return createArrayableGlob( 93 | /** [6] */ 94 | createPresetGlob(globModule.default, { 95 | filesOnly: true 96 | }) 97 | ); 98 | 99 | case 'globby': 100 | /** [2][5] - If `patterns` is a string, wraps into an array. */ 101 | return (patterns, options) => globModule.default([].concat(patterns), options); 102 | 103 | case 'glob': 104 | /** [1][5] */ 105 | return createArrayableGlob( 106 | /** [4] */ 107 | promisify(globModule.default) 108 | ); 109 | 110 | default: 111 | return globModule.default; 112 | } 113 | } catch (err) { 114 | // swallow this error; skip to the next candidate. 115 | } 116 | } 117 | 118 | throw new TypeError( 119 | `No glob library was found, expected one of: ${candidates.join(', ')}` 120 | ); 121 | } 122 | 123 | /** 124 | * Creates a wrapper function for the glob function 125 | * to provide support for arrays of patterns. 126 | * 127 | * @param {function} globFn - The glob function. 128 | * 129 | * @returns {GlobFn} 130 | */ 131 | function createArrayableGlob(globFn) { 132 | return (patterns, options) => { 133 | /** [2] If `patterns` is a string, wraps into an array. */ 134 | patterns = [].concat(patterns); 135 | 136 | const globs = patterns.map((pattern) => globFn(pattern, options)); 137 | 138 | return Promise.all(globs).then((files) => { 139 | return [].concat.apply([], files); 140 | }); 141 | }; 142 | } 143 | 144 | /** 145 | * Creates a wrapper function for the glob function 146 | * to define new default options. 147 | * 148 | * @param {function} globFn - The glob function. 149 | * @param {GlobOptions} presets - The glob function options to preset. 150 | * 151 | * @returns {GlobFn} 152 | */ 153 | function createPresetGlob(globFn, presets) { 154 | return (patterns, options) => globFn(patterns, Object.assign({}, presets, options)); 155 | } 156 | 157 | export default glob; 158 | 159 | export { 160 | glob, 161 | supportsGlob, 162 | }; 163 | -------------------------------------------------------------------------------- /build/helpers/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Provides a decorator for console messages. 3 | */ 4 | 5 | import kleur from 'kleur'; 6 | 7 | /** 8 | * Outputs a message to the console. 9 | * 10 | * @param {string} text - The message to output. 11 | * @param {string} [type] - The type of message. 12 | * @param {string} [timerID] - The console time label to output. 13 | */ 14 | function message(text, type, timerID) { 15 | switch (type) { 16 | case 'success': 17 | console.log('✅ ', kleur.bgGreen().black(text)); 18 | break; 19 | 20 | case 'chore': 21 | console.log('🧹 ', kleur.bgGreen().black(text)); 22 | break; 23 | 24 | case 'notice': 25 | console.log('ℹ️ ', kleur.bgBlue().black(text)); 26 | break; 27 | 28 | case 'error': 29 | console.log('❌ ', kleur.bgRed().black(text)); 30 | break; 31 | 32 | case 'warning': 33 | console.log('⚠️ ', kleur.bgYellow().black(text)); 34 | break; 35 | 36 | case 'waiting': 37 | console.log('⏱ ', kleur.blue().italic(text)); 38 | 39 | if (timerID != null) { 40 | console.timeLog(timerID); 41 | timerID = null; 42 | } 43 | break; 44 | 45 | default: 46 | console.log(text); 47 | break; 48 | } 49 | 50 | if (timerID != null) { 51 | console.timeEnd(timerID); 52 | } 53 | 54 | console.log(''); 55 | } 56 | 57 | export default message; 58 | 59 | export { 60 | message, 61 | }; 62 | -------------------------------------------------------------------------------- /build/helpers/notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Provides a decorator for cross-platform notification. 3 | */ 4 | 5 | import notifier from 'node-notifier'; 6 | 7 | /** 8 | * Sends a cross-platform native notification. 9 | * 10 | * Wraps around node-notifier to assign default values. 11 | * 12 | * @param {string|object} options - The notification options or a message. 13 | * @param {string} options.title - The notification title. 14 | * @param {string} options.message - The notification message. 15 | * @param {string} options.icon - The notification icon. 16 | * @param {function} callback - The notification callback. 17 | * @return {void} 18 | */ 19 | function notification(options, callback) { 20 | if (typeof options === 'string') { 21 | options = { 22 | message: options 23 | }; 24 | } else if (!options.title && !options.message) { 25 | throw new TypeError( 26 | 'Notification expects at least a \'message\' parameter' 27 | ); 28 | } 29 | 30 | if (typeof options.icon === 'undefined') { 31 | options.icon = 'https://user-images.githubusercontent.com/4596862/54868065-c2aea200-4d5e-11e9-9ce3-e0013c15f48c.png'; 32 | } 33 | 34 | // If notification does not use a callback, 35 | // shorten the wait before timing out. 36 | if (typeof callback === 'undefined') { 37 | if (typeof options.wait === 'undefined') { 38 | if (typeof options.timeout === 'undefined') { 39 | options.timeout = 5; 40 | } 41 | } 42 | } 43 | 44 | notifier.notify(options, callback); 45 | } 46 | 47 | export default notification; 48 | 49 | export { 50 | notification, 51 | }; 52 | -------------------------------------------------------------------------------- /build/helpers/postcss.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file If available, returns the PostCSS Processor creator and 3 | * any the Autoprefixer PostCSS plugin. 4 | */ 5 | 6 | /** 7 | * @typedef {import('autoprefixer').autoprefixer.Options} AutoprefixerOptions 8 | */ 9 | 10 | /** 11 | * @typedef {import('postcss').AcceptedPlugin} AcceptedPlugin 12 | */ 13 | 14 | /** 15 | * @typedef {import('postcss').Postcss} Postcss 16 | */ 17 | 18 | /** 19 | * @typedef {import('postcss').ProcessOptions} ProcessOptions 20 | */ 21 | 22 | /** 23 | * @typedef {import('postcss').Processor} Processor 24 | */ 25 | 26 | /** 27 | * @typedef {AcceptedPlugin[]} PluginList 28 | */ 29 | 30 | /** 31 | * @typedef {object} PluginMap 32 | */ 33 | 34 | /** 35 | * @typedef {PluginList|PluginMap} PluginCollection 36 | */ 37 | 38 | /** 39 | * @typedef {object} PostCSSOptions 40 | * 41 | * @property {ProcessOptions} processor - The `Processor#process()` options. 42 | * @property {AutoprefixerOptions} autoprefixer - The `autoprefixer()` options. 43 | */ 44 | 45 | /** 46 | * @type {Postcss|undefined} postcss - The discovered PostCSS function. 47 | * @type {AcceptedPlugin|undefined} autoprefixer - The discovered Autoprefixer function. 48 | */ 49 | let postcss, autoprefixer; 50 | 51 | try { 52 | postcss = await import('postcss'); 53 | postcss = postcss.default; 54 | 55 | autoprefixer = await import('autoprefixer'); 56 | autoprefixer = autoprefixer.default; 57 | } catch (err) { 58 | // do nothing 59 | } 60 | 61 | /** 62 | * @type {boolean} Whether PostCSS was discovered (TRUE) or not (FALSE). 63 | */ 64 | const supportsPostCSS = (typeof postcss === 'function'); 65 | 66 | /** 67 | * @type {PluginList} A list of supported plugins. 68 | */ 69 | const pluginsList = [ 70 | autoprefixer, 71 | ]; 72 | 73 | /** 74 | * @type {PluginMap} A map of supported plugins. 75 | */ 76 | const pluginsMap = { 77 | 'autoprefixer': autoprefixer, 78 | }; 79 | 80 | /** 81 | * Attempts to create a PostCSS Processor with the given plugins and options. 82 | * 83 | * @param {PluginCollection} pluginsListOrMap - A list or map of plugins. 84 | * If a map of plugins, the plugin name looks up `options`. 85 | * @param {PostCSSOptions} options - The PostCSS wrapper options. 86 | * 87 | * @returns {Processor|null} 88 | */ 89 | function createProcessor(pluginsListOrMap, options) 90 | { 91 | if (!postcss) { 92 | return null; 93 | } 94 | 95 | const plugins = parsePlugins(pluginsListOrMap, options); 96 | 97 | return postcss(plugins); 98 | } 99 | 100 | /** 101 | * Parses the PostCSS plugins and options. 102 | * 103 | * @param {PluginCollection} pluginsListOrMap - A list or map of plugins. 104 | * If a map of plugins, the plugin name looks up `options`. 105 | * @param {PostCSSOptions} options - The PostCSS wrapper options. 106 | * 107 | * @returns {PluginList} 108 | */ 109 | function parsePlugins(pluginsListOrMap, options) 110 | { 111 | if (Array.isArray(pluginsListOrMap)) { 112 | return pluginsListOrMap; 113 | } 114 | 115 | /** @type {PluginList} */ 116 | const plugins = []; 117 | 118 | for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) { 119 | if (name in options) { 120 | plugin = plugin[name](options[name]); 121 | } 122 | 123 | plugins.push(plugin); 124 | } 125 | 126 | return plugins; 127 | } 128 | 129 | export default postcss; 130 | 131 | export { 132 | autoprefixer, 133 | createProcessor, 134 | parsePlugins, 135 | pluginsList, 136 | pluginsMap, 137 | postcss, 138 | supportsPostCSS, 139 | }; 140 | -------------------------------------------------------------------------------- /build/helpers/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Provides simple template tags. 3 | */ 4 | 5 | import loconfig from './config.js'; 6 | import { 7 | escapeRegExp, 8 | flatten 9 | } from '../utils/index.js'; 10 | 11 | const templateData = flatten({ 12 | paths: loconfig.paths 13 | }); 14 | 15 | /** 16 | * Replaces all template tags from a map of keys and values. 17 | * 18 | * If replacement pairs contain a mix of substrings, regular expressions, 19 | * and functions, regular expressions are executed last. 20 | * 21 | * @param {*} input - The value being searched and replaced on. 22 | * If input is, or contains, a string, tags will be resolved. 23 | * If input is, or contains, an object, it is mutated directly. 24 | * If input is, or contains, an array, a shallow copy is returned. 25 | * Otherwise, the value is left intact. 26 | * @param {object} [data] - An object in the form `{ 'from': 'to', … }`. 27 | * @return {*} Returns the transformed value. 28 | */ 29 | function resolve(input, data = templateData) { 30 | switch (typeof input) { 31 | case 'string': { 32 | return resolveValue(input, data); 33 | } 34 | 35 | case 'object': { 36 | if (input == null) { 37 | break; 38 | } 39 | 40 | if (Array.isArray(input)) { 41 | return input.map((value) => resolve(value, data)); 42 | } else { 43 | for (const key in input) { 44 | input[key] = resolve(input[key], data); 45 | } 46 | } 47 | } 48 | } 49 | 50 | return input; 51 | } 52 | 53 | /** 54 | * Replaces all template tags in a string from a map of keys and values. 55 | * 56 | * If replacement pairs contain a mix of substrings, regular expressions, 57 | * and functions, regular expressions are executed last. 58 | * 59 | * @param {string} input - The string being searched and replaced on. 60 | * @param {object} [data] - An object in the form `{ 'from': 'to', … }`. 61 | * @return {string} Returns the translated string. 62 | */ 63 | function resolveValue(input, data = templateData) { 64 | const tags = []; 65 | 66 | if (data !== templateData) { 67 | data = flatten(data); 68 | } 69 | 70 | for (let tag in data) { 71 | tags.push(escapeRegExp(tag)); 72 | } 73 | 74 | if (tags.length === 0) { 75 | return input; 76 | } 77 | 78 | const search = new RegExp('\\{%\\s*(' + tags.join('|') + ')\\s*%\\}', 'g'); 79 | return input.replace(search, (match, key) => { 80 | let value = data[key]; 81 | 82 | switch (typeof value) { 83 | case 'function': 84 | /** 85 | * Retrieve the offset of the matched substring `args[0]` 86 | * and the whole string being examined `args[1]`. 87 | */ 88 | let args = Array.prototype.slice.call(arguments, -2); 89 | return value.call(data, match, args[0], args[1]); 90 | 91 | case 'string': 92 | case 'number': 93 | return value; 94 | } 95 | 96 | return ''; 97 | }); 98 | } 99 | 100 | export default resolve; 101 | 102 | export { 103 | resolve, 104 | resolveValue, 105 | }; 106 | -------------------------------------------------------------------------------- /build/tasks/concats.js: -------------------------------------------------------------------------------- 1 | import loconfig from '../helpers/config.js'; 2 | import glob, { supportsGlob } from '../helpers/glob.js'; 3 | import message from '../helpers/message.js'; 4 | import notification from '../helpers/notification.js'; 5 | import resolve from '../helpers/template.js'; 6 | import { merge } from '../utils/index.js'; 7 | import concat from 'concat'; 8 | import { 9 | basename, 10 | normalize, 11 | } from 'node:path'; 12 | 13 | /** 14 | * @const {object} defaultGlobOptions - The default shared glob options. 15 | * @const {object} developmentGlobOptions - The predefined glob options for development. 16 | * @const {object} productionGlobOptions - The predefined glob options for production. 17 | */ 18 | export const defaultGlobOptions = { 19 | }; 20 | export const developmentGlobOptions = Object.assign({}, defaultGlobOptions); 21 | export const productionGlobOptions = Object.assign({}, defaultGlobOptions); 22 | 23 | /** 24 | * @typedef {object} ConcatOptions 25 | * @property {boolean} removeDuplicates - Removes duplicate paths from 26 | * the array of matching files and folders. 27 | * Only the first occurrence of each path is kept. 28 | */ 29 | 30 | /** 31 | * @const {ConcatOptions} defaultConcatOptions - The default shared concatenation options. 32 | * @const {ConcatOptions} developmentConcatOptions - The predefined concatenation options for development. 33 | * @const {ConcatOptions} productionConcatOptions - The predefined concatenation options for production. 34 | */ 35 | export const defaultConcatOptions = { 36 | removeDuplicates: true, 37 | }; 38 | export const developmentConcatOptions = Object.assign({}, defaultConcatOptions); 39 | export const productionConcatOptions = Object.assign({}, defaultConcatOptions); 40 | 41 | /** 42 | * @const {object} developmentConcatFilesArgs - The predefined `concatFiles()` options for development. 43 | * @const {object} productionConcatFilesArgs - The predefined `concatFiles()` options for production. 44 | */ 45 | export const developmentConcatFilesArgs = [ 46 | developmentGlobOptions, 47 | developmentConcatOptions, 48 | ]; 49 | export const productionConcatFilesArgs = [ 50 | productionGlobOptions, 51 | productionConcatOptions, 52 | ]; 53 | 54 | /** 55 | * Concatenates groups of files. 56 | * 57 | * @todo Add support for minification. 58 | * 59 | * @async 60 | * @param {object|boolean} [globOptions=null] - Customize the glob options. 61 | * If `null`, default production options are used. 62 | * If `false`, the glob function will be ignored. 63 | * @param {object} [concatOptions=null] - Customize the concatenation options. 64 | * If `null`, default production options are used. 65 | * @return {Promise} 66 | */ 67 | export default async function concatFiles(globOptions = null, concatOptions = null) { 68 | if (supportsGlob) { 69 | if (globOptions == null) { 70 | globOptions = productionGlobOptions; 71 | } else if ( 72 | globOptions !== false && 73 | globOptions !== developmentGlobOptions && 74 | globOptions !== productionGlobOptions 75 | ) { 76 | globOptions = merge({}, defaultGlobOptions, globOptions); 77 | } 78 | } 79 | 80 | if (concatOptions == null) { 81 | concatOptions = productionConcatOptions; 82 | } else if ( 83 | concatOptions !== developmentConcatOptions && 84 | concatOptions !== productionConcatOptions 85 | ) { 86 | concatOptions = merge({}, defaultConcatOptions, concatOptions); 87 | } 88 | 89 | /** 90 | * @async 91 | * @param {object} entry - The entrypoint to process. 92 | * @param {string[]} entry.includes - One or more paths to process. 93 | * @param {string} entry.outfile - The file to write to. 94 | * @param {?string} [entry.label] - The task label. 95 | * Defaults to the outfile name. 96 | * @return {Promise} 97 | */ 98 | loconfig.tasks.concats?.forEach(async ({ 99 | includes, 100 | outfile, 101 | label = null 102 | }) => { 103 | if (!label) { 104 | label = basename(outfile || 'undefined'); 105 | } 106 | 107 | const timeLabel = `${label} concatenated in`; 108 | console.time(timeLabel); 109 | 110 | try { 111 | if (!Array.isArray(includes)) { 112 | includes = [ includes ]; 113 | } 114 | 115 | includes = resolve(includes); 116 | outfile = resolve(outfile); 117 | 118 | if (supportsGlob && globOptions) { 119 | includes = await glob(includes, globOptions); 120 | } 121 | 122 | if (concatOptions.removeDuplicates) { 123 | includes = includes.map((path) => normalize(path)); 124 | includes = [ ...new Set(includes) ]; 125 | } 126 | 127 | await concat(includes, outfile); 128 | 129 | if (includes.length) { 130 | message(`${label} concatenated`, 'success', timeLabel); 131 | } else { 132 | message(`${label} is empty`, 'notice', timeLabel); 133 | } 134 | } catch (err) { 135 | message(`Error concatenating ${label}`, 'error'); 136 | message(err); 137 | 138 | notification({ 139 | title: `${label} concatenation failed 🚨`, 140 | message: `${err.name}: ${err.message}` 141 | }); 142 | } 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /build/tasks/scripts.js: -------------------------------------------------------------------------------- 1 | import loconfig from '../helpers/config.js'; 2 | import message from '../helpers/message.js'; 3 | import notification from '../helpers/notification.js'; 4 | import resolve from '../helpers/template.js'; 5 | import { merge } from '../utils/index.js'; 6 | import esbuild from 'esbuild'; 7 | import { basename } from 'node:path'; 8 | 9 | /** 10 | * @const {object} defaultESBuildOptions - The default shared ESBuild options. 11 | * @const {object} developmentESBuildOptions - The predefined ESBuild options for development. 12 | * @const {object} productionESBuildOptions - The predefined ESBuild options for production. 13 | */ 14 | export const defaultESBuildOptions = { 15 | bundle: true, 16 | color: true, 17 | sourcemap: true, 18 | target: [ 19 | 'es2015', 20 | ], 21 | }; 22 | export const developmentESBuildOptions = Object.assign({}, defaultESBuildOptions); 23 | export const productionESBuildOptions = Object.assign({}, defaultESBuildOptions, { 24 | logLevel: 'warning', 25 | minify: true, 26 | }); 27 | 28 | /** 29 | * @const {object} developmentScriptsArgs - The predefined `compileScripts()` options for development. 30 | * @const {object} productionScriptsArgs - The predefined `compileScripts()` options for production. 31 | */ 32 | export const developmentScriptsArgs = [ 33 | developmentESBuildOptions, 34 | ]; 35 | export const productionScriptsArgs = [ 36 | productionESBuildOptions, 37 | ]; 38 | 39 | /** 40 | * Bundles and minifies main JavaScript files. 41 | * 42 | * @async 43 | * @param {object} [esBuildOptions=null] - Customize the ESBuild build API options. 44 | * If `null`, default production options are used. 45 | * @return {Promise} 46 | */ 47 | export default async function compileScripts(esBuildOptions = null) { 48 | if (esBuildOptions == null) { 49 | esBuildOptions = productionESBuildOptions; 50 | } else if ( 51 | esBuildOptions !== developmentESBuildOptions && 52 | esBuildOptions !== productionESBuildOptions 53 | ) { 54 | esBuildOptions = merge({}, defaultESBuildOptions, esBuildOptions); 55 | } 56 | 57 | /** 58 | * @async 59 | * @param {object} entry - The entrypoint to process. 60 | * @param {string[]} entry.includes - One or more paths to process. 61 | * @param {string} [entry.outdir] - The directory to write to. 62 | * @param {string} [entry.outfile] - The file to write to. 63 | * @param {?string} [entry.label] - The task label. 64 | * Defaults to the outdir or outfile name. 65 | * @throws {TypeError} If outdir and outfile are missing. 66 | * @return {Promise} 67 | */ 68 | loconfig.tasks.scripts?.forEach(async ({ 69 | includes, 70 | outdir = '', 71 | outfile = '', 72 | label = null 73 | }) => { 74 | if (!label) { 75 | label = basename(outdir || outfile || 'undefined'); 76 | } 77 | 78 | const timeLabel = `${label} compiled in`; 79 | console.time(timeLabel); 80 | 81 | try { 82 | if (!Array.isArray(includes)) { 83 | includes = [ includes ]; 84 | } 85 | 86 | includes = resolve(includes); 87 | 88 | if (outdir) { 89 | outdir = resolve(outdir); 90 | } else if (outfile) { 91 | outfile = resolve(outfile); 92 | } else { 93 | throw new TypeError( 94 | 'Expected \'outdir\' or \'outfile\'' 95 | ); 96 | } 97 | 98 | await esbuild.build(Object.assign({}, esBuildOptions, { 99 | entryPoints: includes, 100 | outdir, 101 | outfile, 102 | })); 103 | 104 | message(`${label} compiled`, 'success', timeLabel); 105 | } catch (err) { 106 | // errors managments (already done in esbuild) 107 | notification({ 108 | title: `${label} compilation failed 🚨`, 109 | message: `${err.errors[0].text} in ${err.errors[0].location.file} line ${err.errors[0].location.line}` 110 | }); 111 | } 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /build/tasks/styles.js: -------------------------------------------------------------------------------- 1 | import loconfig from '../helpers/config.js'; 2 | import message from '../helpers/message.js'; 3 | import notification from '../helpers/notification.js'; 4 | import { 5 | createProcessor, 6 | pluginsMap as postcssPluginsMap, 7 | supportsPostCSS 8 | } from '../helpers/postcss.js'; 9 | import resolve from '../helpers/template.js'; 10 | import { merge } from '../utils/index.js'; 11 | import { writeFile } from 'node:fs/promises'; 12 | import { basename } from 'node:path'; 13 | import * as sass from 'sass'; 14 | import { PurgeCSS } from 'purgecss'; 15 | 16 | let postcssProcessor; 17 | 18 | /** 19 | * @const {object} defaultSassOptions - The default shared Sass options. 20 | * @const {object} developmentSassOptions - The predefined Sass options for development. 21 | * @const {object} productionSassOptions - The predefined Sass options for production. 22 | */ 23 | export const defaultSassOptions = { 24 | sourceMapIncludeSources: true, 25 | sourceMap: true, 26 | }; 27 | 28 | export const developmentSassOptions = Object.assign({}, defaultSassOptions, { 29 | style: 'expanded', 30 | }); 31 | export const productionSassOptions = Object.assign({}, defaultSassOptions, { 32 | style: 'compressed', 33 | }); 34 | 35 | /** 36 | * @const {object} defaultPostCSSOptions - The default shared PostCSS options. 37 | * @const {object} developmentPostCSSOptions - The predefined PostCSS options for development. 38 | * @const {object} productionPostCSSOptions - The predefined PostCSS options for production. 39 | */ 40 | export const defaultPostCSSOptions = { 41 | processor: { 42 | map: { 43 | annotation: false, 44 | inline: false, 45 | sourcesContent: true, 46 | }, 47 | }, 48 | }; 49 | export const developmentPostCSSOptions = Object.assign({}, defaultPostCSSOptions); 50 | export const productionPostCSSOptions = Object.assign({}, defaultPostCSSOptions); 51 | 52 | /** 53 | * @const {object|boolean} developmentStylesArgs - The predefined `compileStyles()` options for development. 54 | * @const {object|boolean} productionStylesArgs - The predefined `compileStyles()` options for production. 55 | */ 56 | export const developmentStylesArgs = [ 57 | developmentSassOptions, 58 | developmentPostCSSOptions, 59 | false 60 | ]; 61 | export const productionStylesArgs = [ 62 | productionSassOptions, 63 | productionPostCSSOptions, 64 | true 65 | ]; 66 | 67 | /** 68 | * Compiles and minifies main Sass files to CSS. 69 | * 70 | * @todo Add deep merge of `postcssOptions` to better support customization 71 | * of default processor options. 72 | * 73 | * @async 74 | * @param {object} [sassOptions=null] - Customize the Sass render API options. 75 | * If `null`, default production options are used. 76 | * @param {object|boolean} [postcssOptions=null] - Customize the PostCSS processor API options. 77 | * If `null`, default production options are used. 78 | * If `false`, PostCSS processing will be ignored. 79 | * @return {Promise} 80 | */ 81 | export default async function compileStyles(sassOptions = null, postcssOptions = null, purge = true) { 82 | if (sassOptions == null) { 83 | sassOptions = productionSassOptions; 84 | } else if ( 85 | sassOptions !== developmentSassOptions && 86 | sassOptions !== productionSassOptions 87 | ) { 88 | sassOptions = merge({}, defaultSassOptions, sassOptions); 89 | } 90 | 91 | if (supportsPostCSS) { 92 | if (postcssOptions == null) { 93 | postcssOptions = productionPostCSSOptions; 94 | } else if ( 95 | postcssOptions !== false && 96 | postcssOptions !== developmentPostCSSOptions && 97 | postcssOptions !== productionPostCSSOptions 98 | ) { 99 | postcssOptions = merge({}, defaultPostCSSOptions, postcssOptions); 100 | } 101 | } 102 | 103 | /** 104 | * @async 105 | * @param {object} entry - The entrypoint to process. 106 | * @param {string[]} entry.infile - The file to process. 107 | * @param {string} entry.outfile - The file to write to. 108 | * @param {?string} [entry.label] - The task label. 109 | * Defaults to the outfile name. 110 | * @return {Promise} 111 | */ 112 | loconfig.tasks.styles?.forEach(async ({ 113 | infile, 114 | outfile, 115 | label = null 116 | }) => { 117 | const filestem = basename((outfile || 'undefined'), '.css'); 118 | 119 | const timeLabel = `${label || `${filestem}.css`} compiled in`; 120 | console.time(timeLabel); 121 | 122 | try { 123 | infile = resolve(infile); 124 | outfile = resolve(outfile); 125 | 126 | let result = sass.compile(infile, sassOptions); 127 | 128 | if (supportsPostCSS && postcssOptions) { 129 | if (typeof postcssProcessor === 'undefined') { 130 | postcssProcessor = createProcessor( 131 | postcssPluginsMap, 132 | postcssOptions 133 | ); 134 | } 135 | 136 | result = await postcssProcessor.process( 137 | result.css, 138 | Object.assign({}, postcssOptions.processor, { 139 | from: outfile, 140 | to: outfile, 141 | }) 142 | ); 143 | 144 | if (result.warnings) { 145 | const warnings = result.warnings(); 146 | if (warnings.length) { 147 | message(`Error processing ${label || `${filestem}.css`}`, 'warning'); 148 | warnings.forEach((warn) => { 149 | message(warn.toString()); 150 | }); 151 | } 152 | } 153 | } 154 | 155 | try { 156 | await writeFile(outfile, result.css).then(() => { 157 | // Purge CSS once file exists. 158 | if (outfile && purge) { 159 | purgeUnusedCSS(outfile, `${label || `${filestem}.css`}`); 160 | } 161 | }); 162 | 163 | if (result.css) { 164 | message(`${label || `${filestem}.css`} compiled`, 'success', timeLabel); 165 | } else { 166 | message(`${label || `${filestem}.css`} is empty`, 'notice', timeLabel); 167 | } 168 | } catch (err) { 169 | message(`Error compiling ${label || `${filestem}.css`}`, 'error'); 170 | message(err); 171 | 172 | notification({ 173 | title: `${label || `${filestem}.css`} save failed 🚨`, 174 | message: `Could not save stylesheet to ${label || `${filestem}.css`}` 175 | }); 176 | } 177 | 178 | if (result.map) { 179 | try { 180 | await writeFile(outfile + '.map', result.map.toString()); 181 | } catch (err) { 182 | message(`Error compiling ${label || `${filestem}.css.map`}`, 'error'); 183 | message(err); 184 | 185 | notification({ 186 | title: `${label || `${filestem}.css.map`} save failed 🚨`, 187 | message: `Could not save sourcemap to ${label || `${filestem}.css.map`}` 188 | }); 189 | } 190 | } 191 | } catch (err) { 192 | message(`Error compiling ${label || `${filestem}.scss`}`, 'error'); 193 | message(err.formatted || err); 194 | 195 | notification({ 196 | title: `${label || `${filestem}.scss`} compilation failed 🚨`, 197 | message: (err.formatted || `${err.name}: ${err.message}`) 198 | }); 199 | } 200 | }); 201 | }; 202 | 203 | /** 204 | * Purge unused styles from CSS files. 205 | * 206 | * @async 207 | * 208 | * @param {string} outfile - The path of a css file 209 | * If missing the function stops. 210 | * @param {string} label - The CSS file label or name. 211 | * @return {Promise} 212 | */ 213 | async function purgeUnusedCSS(outfile, label) { 214 | const contentFiles = loconfig.tasks.purgeCSS?.content; 215 | if (!Array.isArray(contentFiles) || !contentFiles.length) { 216 | return; 217 | } 218 | 219 | label = label ?? basename(outfile); 220 | 221 | const timeLabel = `${label} purged in`; 222 | console.time(timeLabel); 223 | 224 | const purgeCSSResults = await (new PurgeCSS()).purge({ 225 | content: contentFiles, 226 | css: [ outfile ], 227 | defaultExtractor: content => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [], 228 | fontFaces: true, 229 | keyframes: true, 230 | safelist: { 231 | // Keep all except .u-gc-* | .u-margin-* | .u-padding-* 232 | standard: [ /^(?!.*\b(u-gc-|u-margin|u-padding)).*$/ ] 233 | }, 234 | variables: true, 235 | }) 236 | 237 | for (let result of purgeCSSResults) { 238 | await writeFile(outfile, result.css) 239 | 240 | message(`${label} purged`, 'chore', timeLabel); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /build/tasks/svgs.js: -------------------------------------------------------------------------------- 1 | import loconfig from '../helpers/config.js'; 2 | import glob, { supportsGlob } from '../helpers/glob.js'; 3 | import message from '../helpers/message.js'; 4 | import notification from '../helpers/notification.js'; 5 | import { resolve as resolveTemplate } from '../helpers/template.js'; 6 | import { merge } from '../utils/index.js'; 7 | import { 8 | basename, 9 | dirname, 10 | extname, 11 | resolve, 12 | } from 'node:path'; 13 | import commonPath from 'common-path'; 14 | import mixer from 'svg-mixer'; 15 | import slugify from 'url-slug'; 16 | 17 | const basePath = loconfig?.paths?.svgs?.src 18 | ? resolve(loconfig.paths.svgs.src) 19 | : null; 20 | 21 | /** 22 | * @const {object} defaultMixerOptions - The default shared Mixer options. 23 | */ 24 | export const defaultMixerOptions = { 25 | spriteConfig: { 26 | usages: false, 27 | }, 28 | }; 29 | 30 | /** 31 | * @const {object} developmentMixerOptions - The predefined Mixer options for development. 32 | * @const {object} productionMixerOptions - The predefined Mixer options for production. 33 | */ 34 | export const developmentMixerOptions = Object.assign({}, defaultMixerOptions); 35 | export const productionMixerOptions = Object.assign({}, defaultMixerOptions); 36 | 37 | /** 38 | * @const {object} developmentSVGsArgs - The predefined `compileSVGs()` options for development. 39 | * @const {object} productionSVGsArgs - The predefined `compileSVGs()` options for production. 40 | */ 41 | export const developmentSVGsArgs = [ 42 | developmentMixerOptions, 43 | ]; 44 | export const productionSVGsArgs = [ 45 | productionMixerOptions, 46 | ]; 47 | 48 | /** 49 | * Generates and transforms SVG spritesheets. 50 | * 51 | * @async 52 | * @param {object} [mixerOptions=null] - Customize the Mixer API options. 53 | * If `null`, default production options are used. 54 | * @return {Promise} 55 | */ 56 | export default async function compileSVGs(mixerOptions = null) { 57 | if (mixerOptions == null) { 58 | mixerOptions = productionMixerOptions; 59 | } else if ( 60 | mixerOptions !== developmentMixerOptions && 61 | mixerOptions !== productionMixerOptions 62 | ) { 63 | mixerOptions = merge({}, defaultMixerOptions, mixerOptions); 64 | } 65 | 66 | /** 67 | * @async 68 | * @param {object} entry - The entrypoint to process. 69 | * @param {string[]} entry.includes - One or more paths to process. 70 | * @param {string} entry.outfile - The file to write to. 71 | * @param {?string} [entry.label] - The task label. 72 | * Defaults to the outfile name. 73 | * @return {Promise} 74 | */ 75 | loconfig.tasks.svgs?.forEach(async ({ 76 | includes, 77 | outfile, 78 | label = null 79 | }) => { 80 | if (!label) { 81 | label = basename(outfile || 'undefined'); 82 | } 83 | 84 | const timeLabel = `${label} compiled in`; 85 | console.time(timeLabel); 86 | 87 | try { 88 | if (!Array.isArray(includes)) { 89 | includes = [ includes ]; 90 | } 91 | 92 | includes = resolveTemplate(includes); 93 | outfile = resolveTemplate(outfile); 94 | 95 | if (supportsGlob && basePath) { 96 | includes = await glob(includes); 97 | includes = [ ...new Set(includes) ]; 98 | 99 | const common = commonPath(includes); 100 | if (common.commonDir) { 101 | common.commonDir = resolve(common.commonDir); 102 | } 103 | 104 | /** 105 | * Generates the `` attribute and prefix any 106 | * SVG files in subdirectories according to the paths 107 | * common base path. 108 | * 109 | * Example for SVG source path `./assets/images/sprite`: 110 | * 111 | * | Path | ID | 112 | * | ------------------------------------ | --------- | 113 | * | `./assets/images/sprite/foo.svg` | `foo` | 114 | * | `./assets/images/sprite/baz/qux.svg` | `baz-qux` | 115 | * 116 | * @param {string} path - The absolute path to the file. 117 | * @param {string} [query=''] - A query string. 118 | * @return {string} The symbol ID. 119 | */ 120 | mixerOptions.generateSymbolId = (path, query = '') => { 121 | let dirName = dirname(path) 122 | .replace(common.commonDir ?? basePath, '') 123 | .replace(/^\/|\/$/, '') 124 | .replace('/', '-'); 125 | if (dirName) { 126 | dirName += '-'; 127 | } 128 | 129 | const fileName = basename(path, extname(path)); 130 | const decodedQuery = decodeURIComponent(decodeURIComponent(query)); 131 | return `${dirName}${fileName}${slugify(decodedQuery)}`; 132 | }; 133 | } 134 | 135 | const result = await mixer(includes, { 136 | ...mixerOptions, 137 | }); 138 | 139 | await result.write(outfile); 140 | 141 | message(`${label} compiled`, 'success', timeLabel); 142 | } catch (err) { 143 | message(`Error compiling ${label}`, 'error'); 144 | message(err); 145 | 146 | notification({ 147 | title: `${label} compilation failed 🚨`, 148 | message: `${err.name}: ${err.message}` 149 | }); 150 | } 151 | }); 152 | }; 153 | -------------------------------------------------------------------------------- /build/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Provides generic functions and constants. 3 | */ 4 | 5 | /** 6 | * @type {RegExp} - Match all special characters. 7 | */ 8 | const regexUnescaped = /[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g; 9 | 10 | /** 11 | * Quotes regular expression characters. 12 | * 13 | * @param {string} str - The input string. 14 | * @return {string} Returns the quoted (escaped) string. 15 | */ 16 | function escapeRegExp(str) { 17 | return str.replace(regexUnescaped, '\\$&'); 18 | } 19 | 20 | /** 21 | * Creates a new object with all nested object properties 22 | * concatenated into it recursively. 23 | * 24 | * Nested keys are flattened into a property path: 25 | * 26 | * ```js 27 | * { 28 | * a: { 29 | * b: { 30 | * c: 1 31 | * } 32 | * }, 33 | * d: 1 34 | * } 35 | * ``` 36 | * 37 | * ```js 38 | * { 39 | * "a.b.c": 1, 40 | * "d": 1 41 | * } 42 | * ``` 43 | * 44 | * @param {object} input - The object to flatten. 45 | * @param {string} prefix - The parent key prefix. 46 | * @param {object} target - The object that will receive the flattened properties. 47 | * @return {object} Returns the `target` object. 48 | */ 49 | function flatten(input, prefix, target = {}) { 50 | for (const key in input) { 51 | const field = (prefix ? prefix + '.' + key : key); 52 | 53 | if (isObjectLike(input[key])) { 54 | flatten(input[key], field, target); 55 | } else { 56 | target[field] = input[key]; 57 | } 58 | } 59 | 60 | return target; 61 | } 62 | 63 | /** 64 | * Determines whether the passed value is an `Object`. 65 | * 66 | * @param {*} value - The value to be checked. 67 | * @return {boolean} Returns `true` if the value is an `Object`, 68 | * otherwise `false`. 69 | */ 70 | function isObjectLike(value) { 71 | return (value != null && typeof value === 'object'); 72 | } 73 | 74 | /** 75 | * Creates a new object with all nested object properties 76 | * merged into it recursively. 77 | * 78 | * @param {object} target - The target object. 79 | * @param {object[]} ...sources - The source object(s). 80 | * @throws {TypeError} If the target and source are the same. 81 | * @return {object} Returns the `target` object. 82 | */ 83 | function merge(target, ...sources) { 84 | for (const source of sources) { 85 | if (target === source) { 86 | throw new TypeError( 87 | 'Cannot merge, target and source are the same' 88 | ); 89 | } 90 | 91 | for (const key in source) { 92 | if (source[key] != null) { 93 | if (isObjectLike(source[key]) && isObjectLike(target[key])) { 94 | merge(target[key], source[key]); 95 | continue; 96 | } else if (Array.isArray(source[key]) && Array.isArray(target[key])) { 97 | target[key] = target[key].concat(source[key]); 98 | continue; 99 | } 100 | } 101 | 102 | target[key] = source[key]; 103 | } 104 | } 105 | 106 | return target; 107 | } 108 | 109 | export { 110 | escapeRegExp, 111 | flatten, 112 | isObjectLike, 113 | merge, 114 | regexUnescaped, 115 | }; 116 | -------------------------------------------------------------------------------- /build/watch.js: -------------------------------------------------------------------------------- 1 | import concatFiles, { developmentConcatFilesArgs } from './tasks/concats.js'; 2 | import compileScripts, { developmentScriptsArgs } from './tasks/scripts.js'; 3 | import compileStyles, { developmentStylesArgs } from './tasks/styles.js' ; 4 | import compileSVGs, { developmentSVGsArgs } from './tasks/svgs.js'; 5 | import loconfig from './helpers/config.js'; 6 | import message from './helpers/message.js'; 7 | import notification from './helpers/notification.js'; 8 | import resolve from './helpers/template.js'; 9 | import { merge } from './utils/index.js'; 10 | import browserSync from 'browser-sync'; 11 | import { join } from 'node:path'; 12 | 13 | // Match a URL protocol. 14 | const regexUrlStartsWithProtocol = /^[a-z0-9\-]:\/\//i; 15 | 16 | // Build scripts, compile styles, concat files, 17 | // and generate spritesheets on first hit 18 | concatFiles(...developmentConcatFilesArgs); 19 | compileScripts(...developmentScriptsArgs); 20 | compileStyles(...developmentStylesArgs); 21 | compileSVGs(...developmentSVGsArgs); 22 | 23 | // Create a new BrowserSync instance 24 | const server = browserSync.create(); 25 | 26 | // Start the BrowserSync server 27 | server.init(createServerOptions(loconfig), (err) => { 28 | if (err) { 29 | message('Error starting development server', 'error'); 30 | message(err); 31 | 32 | notification({ 33 | title: 'Development server failed', 34 | message: `${err.name}: ${err.message}` 35 | }); 36 | } 37 | }); 38 | 39 | configureServer(server, loconfig); 40 | 41 | /** 42 | * Configures the BrowserSync options. 43 | * 44 | * @param {BrowserSync} server - The BrowserSync API. 45 | * @param {object} loconfig - The project configset. 46 | * @param {object} loconfig.paths - The paths options. 47 | * @param {object} loconfig.tasks - The tasks options. 48 | * @return {void} 49 | */ 50 | function configureServer(server, { paths, tasks }) { 51 | const views = createViewsArray(paths.views); 52 | 53 | // Reload on any changes to views or processed files 54 | server.watch( 55 | [ 56 | ...views, 57 | join(paths.scripts.dest, '*.js'), 58 | join(paths.styles.dest, '*.css'), 59 | join(paths.svgs.dest, '*.svg'), 60 | ] 61 | ).on('change', server.reload); 62 | 63 | // Watch source scripts 64 | server.watch( 65 | [ 66 | join(paths.scripts.src, '**/*.js'), 67 | ] 68 | ).on('change', () => { 69 | compileScripts(...developmentScriptsArgs); 70 | }); 71 | 72 | // Watch source concats 73 | if (tasks.concats?.length) { 74 | server.watch( 75 | resolve( 76 | tasks.concats.reduce( 77 | (patterns, { includes }) => patterns.concat(includes), 78 | [] 79 | ) 80 | ) 81 | ).on('change', () => { 82 | concatFiles(...developmentConcatFilesArgs); 83 | }); 84 | } 85 | 86 | // Watch source styles 87 | server.watch( 88 | [ 89 | join(paths.styles.src, '**/*.scss'), 90 | ] 91 | ).on('change', () => { 92 | compileStyles(...developmentStylesArgs); 93 | }); 94 | 95 | // Watch source SVGs 96 | server.watch( 97 | [ 98 | join(paths.svgs.src, '*.svg'), 99 | ] 100 | ).on('change', () => { 101 | compileSVGs(...developmentSVGsArgs); 102 | }); 103 | } 104 | 105 | /** 106 | * Creates a new object with all the BrowserSync options. 107 | * 108 | * @param {object} loconfig - The project configset. 109 | * @param {object} loconfig.paths - The paths options. 110 | * @param {object} loconfig.server - The server options. 111 | * @return {object} Returns the server options. 112 | */ 113 | function createServerOptions({ 114 | paths, 115 | server: options 116 | }) { 117 | const config = { 118 | open: false, 119 | notify: false, 120 | ghostMode: false 121 | }; 122 | 123 | // Resolve the URL for the BrowserSync server 124 | if (isNonEmptyString(paths.url)) { 125 | // Use proxy 126 | config.proxy = paths.url; 127 | } else if (isNonEmptyString(paths.dest)) { 128 | // Use base directory 129 | config.server = { 130 | baseDir: paths.dest 131 | }; 132 | } 133 | 134 | merge(config, resolve(options)); 135 | 136 | // If HTTPS is enabled, prepend `https://` to proxy URL 137 | if (options?.https) { 138 | if (isNonEmptyString(config.proxy?.target)) { 139 | config.proxy.target = prependSchemeToUrl(config.proxy.target, 'https'); 140 | } else if (isNonEmptyString(config.proxy)) { 141 | config.proxy = prependSchemeToUrl(config.proxy, 'https'); 142 | } 143 | } 144 | 145 | return config; 146 | } 147 | 148 | /** 149 | * Creates a new array (shallow-copied) from the views configset. 150 | * 151 | * @param {*} views - The views configset. 152 | * @throws {TypeError} If views is invalid. 153 | * @return {array} Returns the views array. 154 | */ 155 | function createViewsArray(views) { 156 | if (Array.isArray(views)) { 157 | return Array.from(views); 158 | } 159 | 160 | switch (typeof views) { 161 | case 'string': 162 | return [ views ]; 163 | 164 | case 'object': 165 | if (views != null) { 166 | return Object.values(views); 167 | } 168 | } 169 | 170 | throw new TypeError( 171 | 'Expected \'views\' to be a string, array, or object' 172 | ); 173 | } 174 | 175 | /** 176 | * Prepends the scheme to the URL. 177 | * 178 | * @param {string} url - The URL to mutate. 179 | * @param {string} [scheme] - The URL scheme to prepend. 180 | * @return {string} Returns the mutated URL. 181 | */ 182 | function prependSchemeToUrl(url, scheme = 'http') { 183 | if (regexUrlStartsWithProtocol.test(url)) { 184 | return url.replace(regexUrlStartsWithProtocol, `${scheme}://`); 185 | } 186 | 187 | return `${scheme}://${url}`; 188 | } 189 | 190 | /** 191 | * Determines whether the passed value is a string with at least one character. 192 | * 193 | * @param {*} value - The value to be checked. 194 | * @return {boolean} Returns `true` if the value is a non-empty string, 195 | * otherwise `false`. 196 | */ 197 | function isNonEmptyString(value) { 198 | return (typeof value === 'string' && value.length > 0); 199 | } 200 | -------------------------------------------------------------------------------- /docs/grid.md: -------------------------------------------------------------------------------- 1 | # Grid system 2 | 3 | * [Architectures](#architecture) 4 | * [Build tasks](#build-tasks) 5 | * [Configuration](#configuration) 6 | * [Usage](#usage) 7 | * [Example](#example) 8 | 9 | ## Architecture 10 | 11 | The boilerplate's grid system is meant to be simple and easy to use. The goal is to create a light, flexible, and reusable way to build layouts. 12 | The following styles are needed to work properly: 13 | 14 | * [`o-grid`](../assets/styles/objects/_grid.scss) — Object file where the default grid styles are set such as column numbers, modifiers, and options. 15 | * [`u-grid-columns`](../assets/styles/utilities/_grid-column.scss) — Utility file that generates the styles for every possible column based on an array of media queries and column numbers. 16 | 17 | ### Build tasks 18 | 19 | The columns generated by [`u-grid-columns`](../assets/styles/utilities/_grid-column.scss) adds a lot of styles to the compiled CSS file. To mitigate that, [PurgeCSS] is integrated into the `styles` build task to purge unused CSS. 20 | 21 | #### Configuration 22 | 23 | Depending on your project, you will need to specify all the files that include CSS classes from the grid system. These files will be scanned by [PurgeCSS] to your compiled CSS files. 24 | 25 | Example of a Charcoal project: 26 | 27 | ```jsonc 28 | "purgeCSS": { 29 | "content": [ 30 | "./views/app/template/**/*.mustache", 31 | "./src/App/Template/*.php", 32 | "./assets/scripts/**/*" // use case: `el.classList.add('u-gc-1/2')` 33 | ] 34 | } 35 | ``` 36 | 37 | ## Usage 38 | 39 | The first step is to set intial SCSS values in the following files : 40 | 41 | - [`settings/_config.scss`](../assets/styles/settings/_config.scss) 42 | 43 | ```scss 44 | // Grid 45 | // ========================================================================== 46 | $base-column-nb: 12; 47 | $base-column-gap: $unit-small; 48 | ``` 49 | 50 | You can create multiple column layouts depending on media queries. 51 | 52 | - [`objects/_grid.scss`](../assets/styles/objects/_grid.scss) 53 | 54 | ```scss 55 | .o-grid { 56 | display: grid; 57 | width: 100%; 58 | margin: 0; 59 | padding: 0; 60 | list-style: none; 61 | 62 | // ========================================================================== 63 | // Cols 64 | // ========================================================================== 65 | &.-col-#{$base-column-nb} { 66 | grid-template-columns: repeat(#{$base-column-nb}, 1fr); 67 | } 68 | 69 | &.-col-4 { 70 | grid-template-columns: repeat(4, 1fr); 71 | } 72 | 73 | &.-col-#{$base-column-nb}\@from-md { 74 | @media (min-width: $from-md) { 75 | grid-template-columns: repeat(#{$base-column-nb}, 1fr); 76 | } 77 | } 78 | // … 79 | ``` 80 | 81 | ### Example 82 | 83 | The following layout has 4 columns at `>=999px` and 12 columns at `<1000px`. 84 | 85 | ```html 86 |
87 |

Hello

88 | 89 |
90 |
91 |

This grid has 4 columns and 12 columns from `medium` MQ

92 |
93 | 94 |
95 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

96 |
97 | 98 |
99 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

100 |
101 | 102 |
103 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

104 |
105 |
106 |
107 | ``` 108 | 109 | [PurgeCSS]: https://purgecss.com/ 110 | -------------------------------------------------------------------------------- /docs/technologies.md: -------------------------------------------------------------------------------- 1 | # Technologies 2 | 3 | * [Styles](#styles) 4 | * [CSS Architecture](#css-architecture) 5 | * [CSS Naming Convention](#css-naming-convention) 6 | * [CSS Namespacing](#css-namespacing) 7 | * [Example](#example-1) 8 | * [Scripts](#scripts) 9 | * [Example](#example-2) 10 | * [Page transitions](#page-transitions) 11 | * [Example](#example-3) 12 | * [Scroll detection](#scroll-detection) 13 | * [Example](#example-4) 14 | 15 | ## Styles 16 | 17 | [SCSS][Sass] is a superset of CSS that adds many helpful features to improve 18 | and modularize our styles. 19 | 20 | We use [node-sass] (LibSass) for processing and minifying SCSS into CSS. 21 | 22 | We also use [PostCSS] and [Autoprefixer] to parse our CSS and add 23 | vendor prefixes for experimental features. 24 | 25 | ### CSS Architecture 26 | 27 | The boilerplate's CSS architecture is based on [Inuit CSS][inuitcss] and [ITCSS]. 28 | 29 | * `settings`: Global variables, site-wide settings, config switches, etc. 30 | * `tools`: Site-wide mixins and functions. 31 | * `generic`: Low-specificity, far-reaching rulesets (e.g. resets). 32 | * `elements`: Unclassed HTML elements (e.g. `a {}`, `blockquote {}`, `address {}`). 33 | * `objects`: Objects, abstractions, and design patterns (e.g. `.o-layout {}`). 34 | * `components`: Discrete, complete chunks of UI (e.g. `.c-carousel {}`). 35 | * `utilities`: High-specificity, very explicit selectors. Overrides and helper 36 | classes (e.g. `.u-hidden {}`) 37 | 38 | Learn more about [Inuit CSS](https://github.com/inuitcss/inuitcss#css-directory-structure). 39 | 40 | ### CSS Naming Convention 41 | 42 | We use a simplified [BEM] (Block, Element, Modifier) syntax: 43 | 44 | * `.block` 45 | * `.block_element` 46 | * `.-modifier` 47 | 48 | ### CSS Namespacing 49 | 50 | We namespace our classes for more UI transparency: 51 | 52 | * `o-`: Object that it may be used in any number of unrelated contexts to the one you can currently see it in. Making modifications to these types of class could potentially have knock-on effects in a lot of other unrelated places. 53 | * `c-`: Component is a concrete, implementation-specific piece of UI. All of the changes you make to its styles should be detectable in the context you’re currently looking at. Modifying these styles should be safe and have no side effects. 54 | * `u-`: Utility has a very specific role (often providing only one declaration) and should not be bound onto or changed. It can be reused and is not tied to any specific piece of UI. 55 | * `s-`: Scope creates a new styling context. Similar to a Theme, but not necessarily cosmetic, these should be used sparingly—they can be open to abuse and lead to poor CSS if not used wisely. 56 | * `is-`, `has-`: Is currently styled a certain way because of a state or condition. It tells us that the DOM currently has a temporary, optional, or short-lived style applied to it due to a certain state being invoked. 57 | 58 | Learn about [namespacing](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/). 59 | 60 | ### Example \#1 61 | 62 | ```html 63 |
64 |
65 |
66 |
Heading
67 |
68 |
69 | Button 70 |
71 |
72 |
73 | ``` 74 | 75 | ```scss 76 | .c-block { 77 | &.-large { 78 | padding: rem(60px); 79 | } 80 | } 81 | 82 | .c-block_heading { 83 | @media (max-width: $to-md) { 84 | .c-block.-large & { 85 | margin-bottom: rem(40px); 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | ## Scripts 92 | 93 | We use [esbuild] for bundling and minifying JavaScript/ES modules. 94 | 95 | [modularJS] is a small framework we use on top of ES modules. 96 | 97 | * Automatically init visible modules. 98 | * Easily call other modules methods. 99 | * Quickly set scoped events with delegation. 100 | * Simply select DOM elements scoped in their module. 101 | 102 | [_source_](https://npmjs.com/package/modujs#why) 103 | 104 | ### Example \#2 105 | 106 | ```html 107 |
108 |
109 |

Example

110 |
111 | 112 |
113 | ``` 114 | 115 | ```js 116 | import { module } from 'modujs'; 117 | 118 | export default class extends module { 119 | constructor(m) { 120 | super(m); 121 | 122 | this.events = { 123 | click: { 124 | load: 'loadMore' 125 | } 126 | }; 127 | } 128 | 129 | loadMore() { 130 | this.$('main')[0].classList.add('is-loading'); 131 | } 132 | } 133 | ``` 134 | 135 | Learn more about [modularJS]. 136 | 137 | ## Page transitions 138 | 139 | [modularLoad] is used for page transitions and lazy loading. 140 | 141 | ### Example \#3 142 | 143 | ```html 144 | 148 |
149 | 150 |
151 | ``` 152 | ```js 153 | import modularLoad from 'modularload'; 154 | 155 | this.load = new modularLoad({ 156 | enterDelay: 300, 157 | transitions: { 158 | transitionName: { 159 | enterDelay: 450 160 | } 161 | } 162 | }); 163 | ``` 164 | 165 | Learn more about [modularLoad]. 166 | 167 | ## Scroll detection 168 | 169 | [Locomotive Scroll][locomotive-scroll] is used for elements in viewport 170 | detection and smooth scrolling with parallax. 171 | 172 | ### Example \#4 173 | 174 | ```html 175 |
176 |
Trigger
177 |
Parallax
178 |
179 | ``` 180 | 181 | ```js 182 | import LocomotiveScroll from 'locomotive-scroll'; 183 | 184 | this.scroll = new LocomotiveScroll({}) 185 | ```` 186 | 187 | Learn more about [Locomotive Scroll][locomotive-scroll]. 188 | 189 | [Autoprefixer]: https://npmjs.com/package/autoprefixer 190 | [BEM]: https://bem.info/ 191 | [BrowserSync]: https://npmjs.com/package/browser-sync 192 | [esbuild]: https://npmjs.com/package/esbuild 193 | [inuitcss]: https://github.com/inuitcss/inuitcss 194 | [ITCSS]: https://itcss.io/ 195 | [locomotive-scroll]: https://npmjs.com/package/locomotive-scroll 196 | [modularJS]: https://npmjs.com/package/modujs 197 | [modularLoad]: https://npmjs.com/package/modularload 198 | [node-sass]: https://npmjs.com/package/node-sass 199 | [PostCSS]: https://npmjs.com/package/postcss 200 | [Sass]: https://sass-lang.com/ 201 | [svg-mixer]: https://npmjs.com/package/svg-mixer 202 | [Node]: https://nodejs.org/ 203 | [NPM]: https://npmjs.com/ 204 | [NVM]: https://github.com/nvm-sh/nvm 205 | -------------------------------------------------------------------------------- /loconfig.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "https": { 4 | "key": "~/.config/valet/Certificates/{% paths.url %}.key", 5 | "cert": "~/.config/valet/Certificates/{% paths.url %}.crt" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /loconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "url": "locomotive-boilerplate.test", 4 | "src": "./assets", 5 | "dest": "./www", 6 | "images": { 7 | "src": "./assets/images" 8 | }, 9 | "styles": { 10 | "src": "./assets/styles", 11 | "dest": "./www/assets/styles" 12 | }, 13 | "scripts": { 14 | "src": "./assets/scripts", 15 | "dest": "./www/assets/scripts" 16 | }, 17 | "svgs": { 18 | "src": "./assets/svgs", 19 | "dest": "./www/assets/images" 20 | }, 21 | "views": { 22 | "src": "./www/" 23 | } 24 | }, 25 | "tasks": { 26 | "concats": [ 27 | { 28 | "includes": [ 29 | "{% paths.scripts.src %}/vendors/*.js" 30 | ], 31 | "outfile": "{% paths.scripts.dest %}/vendors.js" 32 | } 33 | ], 34 | "scripts": [ 35 | { 36 | "includes": [ 37 | "{% paths.scripts.src %}/app.js" 38 | ], 39 | "outfile": "{% paths.scripts.dest %}/app.js" 40 | } 41 | ], 42 | "styles": [ 43 | { 44 | "infile": "{% paths.styles.src %}/critical.scss", 45 | "outfile": "{% paths.styles.dest %}/critical.css" 46 | }, 47 | { 48 | "infile": "{% paths.styles.src %}/main.scss", 49 | "outfile": "{% paths.styles.dest %}/main.css" 50 | } 51 | ], 52 | "svgs": [ 53 | { 54 | "includes": [ 55 | "{% paths.svgs.src %}/*.svg" 56 | ], 57 | "outfile": "{% paths.svgs.dest %}/sprite.svg" 58 | } 59 | ], 60 | "purgeCSS": { 61 | "content": [ 62 | "./www/**/*.html", 63 | "./assets/scripts/**/*" 64 | ] 65 | }, 66 | "versions": [ 67 | { 68 | "outfile": "./assets.json" 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@locomotivemtl/boilerplate", 4 | "title": "Locomotive Boilerplate", 5 | "version": "1.0.0", 6 | "author": "Locomotive ", 7 | "type": "module", 8 | "engines": { 9 | "node": ">=20", 10 | "npm": ">=10" 11 | }, 12 | "scripts": { 13 | "start": "node --no-warnings build/watch.js", 14 | "build": "node --no-warnings build/build.js" 15 | }, 16 | "dependencies": { 17 | "locomotive-scroll": "^5.0.0-beta.21", 18 | "modujs": "^1.4.2", 19 | "modularload": "^1.2.6" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^10.4.21", 23 | "browser-sync": "^3.0.4", 24 | "common-path": "^1.0.1", 25 | "concat": "^1.0.3", 26 | "esbuild": "^0.25.8", 27 | "kleur": "^4.1.5", 28 | "node-notifier": "^10.0.1", 29 | "postcss": "^8.5.6", 30 | "purgecss": "^7.0.2", 31 | "sass": "^1.89.2", 32 | "svg-mixer": "^2.3.14", 33 | "tiny-glob": "^0.2.9" 34 | }, 35 | "overrides": { 36 | "browser-sync": { 37 | "ua-parser-js": "^1.0.33" 38 | }, 39 | "svg-mixer": { 40 | "micromatch": "^4.0.8", 41 | "postcss": "^8.4.49" 42 | }, 43 | "svg-mixer-utils": { 44 | "anymatch": "^3.1.3" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /www/assets/emails/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Email | Locomotive Boilerplate 6 | 7 | 8 |
9 | 10 | 11 | 32 | 33 |
12 | 13 | 14 | 29 | 30 |
15 | 16 | 17 | 26 | 27 |
18 |

Heading 1

19 |

Heading 2

20 |

Heading 3

21 |

Heading 4

22 |

23 | After you enter your content, highlight the text you want to style and select the options you set in the style editor in the "styles" drop down box. Want to 24 |

25 |
28 |
31 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /www/assets/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/fonts/.gitkeep -------------------------------------------------------------------------------- /www/assets/fonts/SourceSans3-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/fonts/SourceSans3-Bold.woff2 -------------------------------------------------------------------------------- /www/assets/fonts/SourceSans3-BoldIt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/fonts/SourceSans3-BoldIt.woff2 -------------------------------------------------------------------------------- /www/assets/fonts/SourceSans3-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/fonts/SourceSans3-Regular.woff2 -------------------------------------------------------------------------------- /www/assets/fonts/SourceSans3-RegularIt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/fonts/SourceSans3-RegularIt.woff2 -------------------------------------------------------------------------------- /www/assets/images/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/images/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /www/assets/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /www/assets/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /www/assets/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /www/assets/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /www/assets/images/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /www/assets/images/sprite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/assets/scripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/scripts/.gitkeep -------------------------------------------------------------------------------- /www/assets/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/assets/styles/.gitkeep -------------------------------------------------------------------------------- /www/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/locomotive-boilerplate/6f04e21146b8eaeb19d544c4d02d98c40a7ca39a/www/favicon.ico -------------------------------------------------------------------------------- /www/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form | Locomotive Boilerplate 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Locomotive Boilerplate

24 | 31 |
32 | 33 |
34 |
35 |

Page

36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 |
69 |
70 | 71 |
72 | 77 |
78 |
79 |
80 | 81 | 82 |
83 | 86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 |

Made with 🚂

95 |
96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /www/grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Locomotive Boilerplate 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 29 | 30 | 31 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | 40 |

Locomotive Boilerplate

41 |
42 | 49 |
50 | 51 |
52 |
53 |

Hello

54 | 55 |
56 |
57 |

This grid has 4 columns and 12 columns from `medium` MQ

58 |
59 | 60 |
61 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

62 |
63 | 64 |
65 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

66 |
67 | 68 |
69 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

70 |
71 | 72 |
73 |

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita provident distinctio deleniti eaque cumque doloremque aut quo dicta porro commodi, temporibus totam dolor autem tempore quasi ullam sed suscipit vero?

74 |
75 |
76 |
77 |
78 | 79 |
80 |

Made with 🚂

82 |
83 |
84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /www/images.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Images | Locomotive Boilerplate 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Locomotive Boilerplate

24 | 31 |
32 | 33 |
34 |
35 |

Images

36 | 37 |
38 |

Lazy load demo

39 | 40 |

Basic

41 | 42 |
43 |
44 |
45 |
46 | 47 |

Using o-ratio & background-image

48 | 49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |

Relative to scroll

57 | 58 |

Using o-ratio & img

59 | 60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 |
77 | 78 |

Using o-ratio & background-image

79 | 80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | 88 |

Using SVG viewport for ratio

89 | 90 |
91 | 98 | 99 | 106 | 107 | 114 |
115 |
116 |
117 |
118 | 119 |
120 |

Made with 🚂

121 |
122 |
123 |
124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Locomotive Boilerplate 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 | 48 |

Locomotive Boilerplate

49 |
50 | 57 |
58 | 59 |
60 |
61 |

Hello

62 |
63 |
64 | 65 |
66 |

Made with 🚂

68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /www/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Locomotive Boilerplate", 3 | "short_name": "Boilerplate", 4 | "icons": [ 5 | { 6 | "src": "assets/images/favicons/android-chrome-144x144.png", 7 | "sizes": "144x144", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | --------------------------------------------------------------------------------