1
0
mirror of https://github.com/locomotivemtl/locomotive-boilerplate.git synced 2026-01-15 00:55:08 +08:00

1 Commits

Author SHA1 Message Date
Lucas Vallenet
b026bdb809 New Typography prefix and default styles 2022-05-02 11:30:04 +02:00
110 changed files with 17066 additions and 7771 deletions

8
.gitignore vendored
View File

@@ -3,11 +3,3 @@ node_modules
Thumbs.db
loconfig.*.json
!loconfig.example.json
.prettierrc
www/assets/scripts/*
!www/assets/scripts/.gitkeep
www/assets/styles/*
!www/assets/styles/.gitkeep
assets.json

2
.nvmrc
View File

@@ -1 +1 @@
v20
v14.17

View File

@@ -15,7 +15,6 @@
* Uses [SVG Mixer] for processing SVG files and generating spritesheets.
* Uses [ITCSS] for a sane and scalable CSS architecture.
* Uses [Locomotive Scroll] for smooth scrolling with parallax effects.
* Uses a custom [grid system](docs/grid.md) for layout creation.
Learn more about [languages and technologies](docs/technologies.md).
@@ -23,8 +22,8 @@ Learn more about [languages and technologies](docs/technologies.md).
Make sure you have the following installed:
* [Node] — at least 20, the latest LTS is recommended.
* [NPM] — at least 10, the latest LTS is recommended.
* [Node] — at least 14.17, the latest LTS is recommended.
* [NPM] — at least 6.0, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
@@ -85,7 +84,6 @@ Learn more about [development and building](docs/development.md).
* [Development and building](docs/development.md)
* [Languages and technologies](docs/technologies.md)
* [Grid system](docs/grid.md)
[BrowserSync]: https://npmjs.com/package/browser-sync
[ESBuild]: https://npmjs.com/package/esbuild

View File

@@ -1,137 +1,35 @@
import modular from 'modujs';
import * as modules from './modules';
import globals from './globals';
import { debounce } from './utils/tickers';
import { $html } from './utils/dom';
import { ENV, FONT, CUSTOM_EVENT, CSS_CLASS } from './config'
import { isFontLoadingAPIAvailable, loadFonts } from './utils/fonts';
import { html } from './utils/environment';
const app = new modular({
modules,
modules: modules
});
function init() {
bindEvents();
globals();
setViewportSizes();
app.init(app);
$html.classList.add(CSS_CLASS.LOADED, CSS_CLASS.READY);
$html.classList.remove(CSS_CLASS.LOADING);
/**
* Debug focus
*/
// document.addEventListener(
// "focusin",
// function () {
// console.log('focused: ', document.activeElement)
// }, true
// );
/**
* Eagerly load the following fonts.
*/
if (isFontLoadingAPIAvailable) {
loadFonts(FONT.EAGER, ENV.IS_DEV).then((eagerFonts) => {
$html.classList.add(CSS_CLASS.FONTS_LOADED);
/**
* Debug fonts loading
*/
// if (ENV.IS_DEV) {
// console.group('Eager fonts loaded!', eagerFonts.length, '/', document.fonts.size);
// console.group('State of eager fonts:');
// eagerFonts.forEach(font => console.log(font.family, font.style, font.weight, font.status));
// console.groupEnd();
// console.group('State of all fonts:');
// document.fonts.forEach(font => console.log(font.family, font.style, font.weight, font.status));
// console.groupEnd();
// }
});
}
}
////////////////
// Global events
////////////////
function bindEvents() {
// Resize event
const resizeEndEvent = new CustomEvent(CUSTOM_EVENT.RESIZE_END)
window.addEventListener(
"resize",
debounce(() => {
window.dispatchEvent(resizeEndEvent)
}, 200, false)
)
window.addEventListener(
"resize",
onResize
)
}
function onResize() {
setViewportSizes()
}
function setViewportSizes() {
// Document styles
const documentStyles = document.documentElement.style;
// Viewport width
const vw = document.body.clientWidth * 0.01;
documentStyles.setProperty('--vw', `${vw}px`);
// Return if browser supports vh, svh, dvh, & lvh
if (ENV.SUPPORTS_VH) {
return
}
// Viewport height
const svh = document.documentElement.clientHeight * 0.01;
documentStyles.setProperty('--svh', `${svh}px`);
const dvh = window.innerHeight * 0.01;
documentStyles.setProperty('--dvh', `${dvh}px`);
if (document.body) {
const fixed = document.createElement('div');
fixed.style.width = '1px';
fixed.style.height = '100vh';
fixed.style.position = 'fixed';
fixed.style.left = '0';
fixed.style.top = '0';
fixed.style.bottom = '0';
fixed.style.visibility = 'hidden';
document.body.appendChild(fixed);
var fixedHeight = fixed.clientHeight;
fixed.remove();
const lvh = fixedHeight * 0.01;
documentStyles.setProperty('--lvh', `${lvh}px`);
}
}
////////////////
// Execute
////////////////
window.addEventListener('load', () => {
window.onload = (event) => {
const $style = document.getElementById('main-css');
if ($style) {
if ($style.isLoaded) {
init();
} else {
$style.addEventListener('load', init);
$style.addEventListener('load', (event) => {
init();
});
}
} else {
console.warn('The "main-css" stylesheet not found');
}
});
};
function init() {
globals();
app.init(app);
html.classList.add('is-loaded');
html.classList.add('is-ready');
html.classList.remove('is-loading');
}

View File

@@ -1,65 +0,0 @@
/**
* > When using the esBuild API, all `process.env.NODE_ENV` expressions
* > are automatically defined to `"production"` if all minification
* > options are enabled and `"development"` otherwise. This only happens
* > if `process`, `process.env`, and `process.env.NODE_ENV` are not already
* > defined. This substitution is necessary to avoid code crashing instantly
* > (since `process` is a Node API, not a web API).
* > — https://esbuild.github.io/api/#platform
*/
const NODE_ENV = process.env.NODE_ENV
const IS_MOBILE = window.matchMedia('(any-pointer:coarse)').matches
// Main environment variables
const ENV = Object.freeze({
// Node environment
NAME: NODE_ENV,
IS_PROD: NODE_ENV === 'production',
IS_DEV: NODE_ENV === 'development',
// Device
IS_MOBILE,
IS_DESKTOP: !IS_MOBILE,
// Supports
SUPPORTS_VH: (
'CSS' in window
&& 'supports' in window.CSS
&& window.CSS.supports('height: 100svh')
&& window.CSS.supports('height: 100dvh')
&& window.CSS.supports('height: 100lvh')
)
})
// Main CSS classes used within the project
const CSS_CLASS = Object.freeze({
LOADING: 'is-loading',
LOADED: 'is-loaded',
READY: 'is-ready',
FONTS_LOADED: 'fonts-loaded',
LAZY_CONTAINER: 'c-lazy',
LAZY_LOADED: '-lazy-loaded',
// ...
})
// Custom js events
const CUSTOM_EVENT = Object.freeze({
RESIZE_END: 'loco.resizeEnd',
// ...
})
// Fonts parameters
const FONT = Object.freeze({
EAGER: [
{ family: 'Source Sans', style: 'normal', weight: 400 },
{ family: 'Source Sans', style: 'normal', weight: 700 },
],
})
export {
ENV,
CSS_CLASS,
CUSTOM_EVENT,
FONT,
}

View File

@@ -1,17 +1,5 @@
import { ENV } from './config';
import svg4everybody from 'svg4everybody';
// Dynamic imports for development mode only
let gridHelper;
(async () => {
if (ENV.IS_DEV) {
const gridHelperModule = await import('./utils/grid-helper');
gridHelper = gridHelperModule?.gridHelper;
}
})();
export default function () {
/**
* Add grid helper
*/
gridHelper?.();
export default function() {
svg4everybody();
}

View File

@@ -1,3 +1,2 @@
export {default as Example} from './modules/Example';
export {default as Load} from './modules/Load';
export {default as Scroll} from './modules/Scroll';

View File

@@ -1,6 +1,4 @@
import { module } from 'modujs';
import { FONT } from '../config';
import { whenReady } from '../utils/fonts';
export default class extends module {
constructor(m) {
@@ -8,10 +6,5 @@ export default class extends module {
}
init() {
whenReady(FONT.EAGER).then((fonts) => this.onFontsLoaded(fonts));
}
onFontsLoaded(fonts) {
console.log('Example: Eager Fonts Loaded!', fonts)
}
}

View File

@@ -1,6 +1,6 @@
import { module } from 'modujs'
import { lazyLoadImage } from '../utils/image'
import LocomotiveScroll from 'locomotive-scroll'
import { module } from 'modujs';
import { lazyLoadImage } from '../utils/image';
import LocomotiveScroll from 'locomotive-scroll';
export default class extends module {
constructor(m) {
@@ -9,14 +9,18 @@ export default class extends module {
init() {
this.scroll = new LocomotiveScroll({
modularInstance: this,
})
el: this.el,
smooth: true
});
// // Force scroll to top
// if (history.scrollRestoration) {
// history.scrollRestoration = 'manual'
// window.scrollTo(0, 0)
// }
this.scroll.on('call', (func, way, obj, id) => {
// Using modularJS
this.call(func[0], { way, obj }, func[1], func[2]);
});
this.scroll.on('scroll', (args) => {
// console.log(args.scroll);
})
}
/**
@@ -37,22 +41,11 @@ export default class extends module {
* @param {LocomotiveScroll} args - The Locomotive Scroll instance.
*/
lazyLoad(args) {
lazyLoadImage(args.target, null, () => {
lazyLoadImage(args.obj.el, null, () => {
//callback
})
}
scrollTo(params) {
let { target, ...options } = params
options = Object.assign({
// Defaults
duration: 1,
}, options)
this.scroll?.scrollTo(target, options)
}
destroy() {
this.scroll.destroy();
}

View File

@@ -0,0 +1,101 @@
import { isArray } from './is';
export function addToArray ( array, value ) {
const index = array.indexOf( value );
if ( index === -1 ) {
array.push( value );
}
}
export function arrayContains ( array, value ) {
for ( let i = 0, c = array.length; i < c; i++ ) {
if ( array[i] == value ) {
return true;
}
}
return false;
}
export function arrayContentsMatch ( a, b ) {
let i;
if ( !isArray( a ) || !isArray( b ) ) {
return false;
}
if ( a.length !== b.length ) {
return false;
}
i = a.length;
while ( i-- ) {
if ( a[i] !== b[i] ) {
return false;
}
}
return true;
}
export function ensureArray ( x ) {
if ( typeof x === 'string' ) {
return [ x ];
}
if ( x === undefined ) {
return [];
}
return x;
}
export function lastItem ( array ) {
return array[ array.length - 1 ];
}
export function removeFromArray ( array, member ) {
if ( !array ) {
return;
}
const index = array.indexOf( member );
if ( index !== -1 ) {
array.splice( index, 1 );
}
}
export function toArray ( arrayLike ) {
const array = [];
let i = arrayLike.length;
while ( i-- ) {
array[i] = arrayLike[i];
}
return array;
}
export function findByKeyValue( array, key, value ) {
return array.filter(function( obj ) {
return obj[key] === value;
});
}
export function cloneArray( array ) {
return JSON.parse(JSON.stringify(array));
}
/**
* Shuffles array in place. ES6 version
* @param {Array} a items An array containing the items.
*/
export function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}

View File

@@ -0,0 +1,15 @@
export default function(func, wait, immediate) {
let timeout;
return function() {
const context = this;
const args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}

View File

@@ -1,7 +0,0 @@
const $html = document.documentElement
const $body = document.body
export {
$html,
$body,
}

View File

@@ -0,0 +1,8 @@
const APP_NAME = 'Boilerplate';
const DATA_API_KEY = '.data-api';
const html = document.documentElement;
const body = document.body;
const isDebug = html.hasAttribute('data-debug');
export { APP_NAME, DATA_API_KEY, html, body, isDebug };

View File

@@ -1,402 +0,0 @@
/**
* Font Faces
*
* Provides utilities to facilitate interactions with the CSS Font Loading API.
*
* Features functions to:
*
* - Retrieve one or more `FontFace` instances based on a font search query.
* - Check if a `FontFace` instance matches a font search query.
* - Eagerly load fonts that match a font search query.
* - Wait until fonts that match a font search query are loaded.
*
* References:
*
* - {@link https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API}
*/
/**
* @typedef {Object} FontFaceReference
*
* @property {string} family - The name used to identify the font in our CSS.
* @property {string} [style] - The style used by the font in our CSS.
* @property {string} [weight] - The weight used by the font in our CSS.
*/
const isFontLoadingAPIAvailable = ('fonts' in document);
/**
* Determines if the given font matches the given `FontFaceReference`.
*
* @param {FontFace} font - The font to inspect.
* @param {FontFaceReference} criterion - The object of property values to match.
*
* @returns {boolean}
*/
function conformsToReference(font, criterion)
{
for (const [ key, value ] of Object.entries(criterion)) {
switch (key) {
case 'family': {
if (trim(font[key]) !== value) {
return false;
}
break;
}
case 'weight': {
/**
* Note concerning font weights:
* Loose equality (`==`) is used to compare numeric weights,
* a number (`400`) and a numeric string (`"400"`).
* Comparison between numeric and keyword values is neglected.
*
* @link https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
*/
if (font[key] != value) {
return false;
}
break;
}
default: {
if (font[key] !== value) {
return false;
}
break;
}
}
}
return true;
}
/**
* Determines if the given font matches the given font shorthand.
*
* @param {FontFace} font - The font to inspect.
* @param {string} criterion - The font shorthand to match.
*
* @returns {boolean}
*/
function conformsToShorthand(font, criterion)
{
const family = trim(font.family);
if (trim(family) === criterion) {
return true;
}
if (
criterion.endsWith(trim(family)) && (
criterion.match(font.weight) ||
criterion.match(font.style)
)
) {
return true;
}
return true;
}
/**
* Determines if the given font matches any of the given criteria.
*
* @param {FontFace} font - The font to inspect.
* @param {FontFaceReference[]} criteria - A list of objects with property values to match.
*
* @returns {boolean}
*/
function conformsToAnyReference(font, criteria)
{
for (const criterion of criteria) {
if (conformsToReference(font, criterion)) {
return true;
}
}
return false;
}
/**
* Returns an iterator of all `FontFace` from `document.fonts` that satisfy
* the provided `FontFaceReference`.
*
* @param {FontFaceReference} font
*
* @returns {FontFace[]}
*/
function findManyByReference(search)
{
const found = [];
for (const font of document.fonts) {
if (conformsToReference(font, search)) {
found.push(font);
}
}
return found;
}
/**
* Returns an iterator of all `FontFace` from `document.fonts` that satisfy
* the provided font shorthand.
*
* @param {string} font
*
* @returns {FontFace[]}
*/
function findManyByShorthand(search)
{
const found = [];
for (const font of document.fonts) {
if (conformsToShorthand(font, search)) {
found.push(font);
}
}
return found;
}
/**
* Returns the first `FontFace` from `document.fonts` that satisfies
* the provided `FontFaceReference`.
*
* @param {FontFaceReference} font
*
* @returns {?FontFace}
*/
function findOneByReference(search)
{
for (const font of document.fonts) {
if (conformsToReference(font, criterion)) {
return font;
}
}
return null;
}
/**
* Returns the first `FontFace` from `document.fonts` that satisfies
* the provided font shorthand.
*
* Examples:
*
* - "Roboto"
* - "italic bold 16px Roboto"
*
* @param {string} font
*
* @returns {?FontFace}
*/
function findOneByShorthand(search)
{
for (const font of document.fonts) {
if (conformsToShorthand(font, search)) {
return font;
}
}
return null;
}
/**
* Returns a `FontFace` from `document.fonts` that satisfies
* the provided query.
*
* @param {FontFaceReference|string} font - Either:
* - a `FontFaceReference` object
* - a font family name
* - a font specification, for example "italic bold 16px Roboto"
*
* @returns {?FontFace}
*
* @throws {TypeError}
*/
function getAny(search) {
if (search) {
switch (typeof search) {
case 'string':
return findOneByShorthand(search);
case 'object':
return findOneByReference(search);
}
}
throw new TypeError(
'Expected font query to be font shorthand or font reference'
);
}
/**
* Returns an iterator of all `FontFace` from `document.fonts` that satisfy
* the provided queries.
*
* @param {FontFaceReference|string|(FontFaceReference|string)[]} queries
*
* @returns {FontFace[]}
*
* @throws {TypeError}
*/
function getMany(queries) {
if (!Array.isArray(queries)) {
queries = [ queries ];
}
const found = new Set();
queries.forEach((search) => {
if (search) {
switch (typeof search) {
case 'string':
found.add(...findManyByShorthand(search));
return;
case 'object':
found.add(...findManyByReference(search));
return;
}
}
throw new TypeError(
'Expected font query to be font shorthand or font reference'
);
})
return [ ...found ];
}
/**
* Determines if a font face is registered.
*
* @param {FontFace|FontFaceReference|string} search - Either:
* - a `FontFace` instance
* - a `FontFaceReference` object
* - a font family name
* - a font specification, for example "italic bold 16px Roboto"
*
* @returns {boolean}
*/
function hasAny(search) {
if (search instanceof FontFace) {
return document.fonts.has(search);
}
return getAny(search) != null;
}
/**
* Eagerly load fonts.
*
* Most user agents only fetch and load fonts when they are first needed
* ("lazy loaded"), which can result in a perceptible delay.
*
* This function will "eager load" the fonts.
*
* @param {(FontFace|FontFaceReference)[]} fontsToLoad - List of fonts to load.
* @param {boolean} [debug] - If TRUE, log details to the console.
*
* @returns {Promise}
*/
async function loadFonts(fontsToLoad, debug = false)
{
if ((fontsToLoad.size ?? fontsToLoad.length) === 0) {
throw new TypeError(
'Expected at least one font'
);
}
return await loadFontsWithAPI([ ...fontsToLoad ], debug);
}
/**
* Eagerly load a font using `FontFaceSet` API.
*
* @param {FontFace} font
*
* @returns {Promise}
*/
async function loadFontFaceWithAPI(font)
{
return await (font.status === 'unloaded'
? font.load()
: font.loaded
).then((font) => font, (err) => font)
}
/**
* Eagerly load fonts using `FontFaceSet` API.
*
* @param {FontFaceReference[]} fontsToLoad
* @param {boolean} [debug]
*
* @returns {Promise}
*/
async function loadFontsWithAPI(fontsToLoad, debug = false)
{
debug && console.group('[loadFonts:API]', fontsToLoad.length, '/', document.fonts.size);
const fontsToBeLoaded = [];
for (const fontToLoad of fontsToLoad) {
if (fontToLoad instanceof FontFace) {
if (!document.fonts.has(fontToLoad)) {
document.fonts.add(fontToLoad);
}
fontsToBeLoaded.push(
loadFontFaceWithAPI(fontToLoad)
);
} else {
fontsToBeLoaded.push(
...getMany(fontToLoad).map((font) => loadFontFaceWithAPI(font))
);
}
}
debug && console.groupEnd();
return await Promise.all(fontsToBeLoaded);
}
/**
* Removes quotes from the the string.
*
* When a `@font-face` is declared, the font family is sometimes
* defined in quotes which end up included in the `FontFace` instance.
*
* @param {string} value
*
* @returns {string}
*/
function trim(value) {
return value.replace(/['"]+/g, '');
}
/**
* Returns a Promise that resolves with the specified fonts
* when they are done loading or failed.
*
* @param {FontFaceReference|string|(FontFaceReference|string)[]} queries
*
* @returns {Promise}
*/
async function whenReady(queries)
{
const fonts = getMany(queries);
return await Promise.all(fonts.map((font) => font.loaded));
}
export {
getAny,
getMany,
hasAny,
isFontLoadingAPIAvailable,
loadFonts,
whenReady,
}

View File

@@ -1,138 +0,0 @@
/**
* Grid Helper
*
* Provides a grid based on the design guidelines and is helpful for web integration.
*
* - `Control + g` to toggle the grid
*
*/
/**
* @typedef {Object} GridHelperReference
*
* @property {string} [gutterCssVar=GRID_HELPER_GUTTER_CSS_VAR] - CSS variable used to define grid gutters.
* @property {string} [marginCssVar=GRID_HELPER_MARGIN_CSS_VAR] - CSS variable used to define grid margins.
* @property {string} [rgbaColor=GRID_HELPER_RGBA_COLOR] - RGBA color for the grid appearence.
*/
const GRID_HELPER_GUTTER_CSS_VAR = '--grid-gutter';
const GRID_HELPER_MARGIN_CSS_VAR = '--grid-margin';
const GRID_HELPER_RGBA_COLOR = 'rgba(255, 0, 0, .1)';
/**
* Create a grid helper
*
* @param {GridHelperReference}
*
*/
function gridHelper({
gutterCssVar = GRID_HELPER_GUTTER_CSS_VAR,
marginCssVar = GRID_HELPER_MARGIN_CSS_VAR,
rgbaColor = GRID_HELPER_RGBA_COLOR,
} = {}) {
// Set grid container
const $gridContainer = document.createElement('div');
document.body.append($gridContainer);
// Set grid appearence
setGridHelperColumns($gridContainer, rgbaColor);
setGridHelperStyles($gridContainer, gutterCssVar, marginCssVar);
// Set grid interactivity
setGridEvents($gridContainer, rgbaColor);
}
/**
* Set grid container styles
*
* @param {HTMLElement} $container - DOM Element that contains a list of generated columns
* @param {string} gutterCssVar - CSS variable used to define grid gutters.
* @param {string} marginCssVar - CSS variable used to define grid margins.
*
*/
function setGridHelperStyles($container, gutterCssVar, marginCssVar) {
const elStyles = $container.style;
elStyles.zIndex = '10000';
elStyles.position = 'fixed';
elStyles.top = '0';
elStyles.left = '0';
elStyles.display = 'flex';
elStyles.width = '100%';
elStyles.height = '100%';
elStyles.columnGap = `var(${gutterCssVar}, 0)`;
elStyles.paddingLeft = `var(${marginCssVar}, 0)`;
elStyles.paddingRight = `var(${marginCssVar}, 0)`;
elStyles.pointerEvents = 'none';
elStyles.visibility = 'hidden';
}
/**
* Set grid columns
*
* @param {HTMLElement} $container - DOM Element that will contain a list of generated columns
* @param {string} rgbaColor - RGBA color to stylize the generated columns
*
*/
function setGridHelperColumns($container, rgbaColor) {
// Clear columns
$container.innerHTML = '';
// Loop through columns
const columns = Number(
window.getComputedStyle($container).getPropertyValue('--grid-columns')
);
let $col;
for (var i = 0; i < columns; i++) {
$col = document.createElement('div');
$col.style.flex = '1 1 0';
$col.style.backgroundColor = rgbaColor;
$container.appendChild($col);
}
}
/**
* Set grid events
*
* Resize to rebuild columns
* Keydown/Keyup to toggle the grid display
*
* @param {HTMLElement} $container - DOM Element that contains a list of generated columns
* @param {string} rgbaColor - RGBA color to stylize the generated columns
*
*/
function setGridEvents($container, rgbaColor) {
// Handle resize
window.addEventListener(
'resize',
setGridHelperColumns($container, rgbaColor)
);
// Toggle grid
let ctrlDown = false;
let isActive = false;
document.addEventListener('keydown', (e) => {
if (e.key == 'Control') {
ctrlDown = true;
} else {
if (ctrlDown && e.key == 'g') {
if (isActive) {
$container.style.visibility = 'hidden';
} else {
$container.style.visibility = 'visible';
}
isActive = !isActive;
}
}
});
document.addEventListener('keyup', (e) => {
if (e.key == 'Control') {
ctrlDown = false;
}
});
}
export { gridHelper };

View File

@@ -1,140 +1,141 @@
/**
* Escape HTML string
* @param {string} str - string to escape
* @return {string} escaped string
* @see https://github.com/ractivejs/ractive/blob/dev/src/utils/html.js
*/
const escapeHtml = str =>
str.replace(/[&<>'"]/g, tag => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
}[tag]))
export function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* Unescape HTML string
* @param {string} str - string to unescape
* @return {string} unescaped string
* Prepare HTML content that contains mustache characters for use with Ractive
* @param {string} str
* @return {string}
*/
const unescapeHtml = str =>
str.replace('&amp;', '&')
.replace('&lt;', '<')
.replace('&gt;', '>')
.replace('&#39;', "'")
.replace('&quot;', '"')
export function unescapeHtml(str) {
return str
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
}
/**
* Get element data attributes
* @param {HTMLElement} node - node element
* @return {array} node data
* @param {DOMElement} node
* @return {Array} data
*/
const getNodeData = node => {
export function getNodeData(node) {
// All attributes
const attributes = node.attributes
const attributes = node.attributes;
// Regex Pattern
const pattern = /^data\-(.+)$/
const pattern = /^data\-(.+)$/;
// Output
const data = {}
const data = {};
for (let i in attributes) {
if (!attributes[i]) {
continue
continue;
}
// Attributes name (ex: data-module)
let name = attributes[i].name
let name = attributes[i].name;
// This happens.
if (!name) {
continue
continue;
}
let match = name.match(pattern)
let match = name.match(pattern);
if (!match) {
continue
continue;
}
// If this throws an error, you have some
// serious problems in your HTML.
data[match[1]] = getData(node.getAttribute(name))
data[match[1]] = getData(node.getAttribute(name));
}
return data;
}
const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/;
/**
* Parse value to data type.
*
* @link https://github.com/jquery/jquery/blob/3.1.1/src/data.js
* @param {string} data - value to convert
* @return {mixed} value in its natural data type
* @param {string} data - A value to convert.
* @return {mixed} Returns the value in its natural data type.
*/
const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/
const getData = data => {
export function getData(data) {
if (data === 'true') {
return true
return true;
}
if (data === 'false') {
return false
return false;
}
if (data === 'null') {
return null
return null;
}
// Only convert to a number if it doesn't change the string
if (data === +data+'') {
return +data
return +data;
}
if (rbrace.test(data)) {
return JSON.parse(data)
if (rbrace.test( data )) {
return JSON.parse( data );
}
return data
return data;
}
/**
* Returns an array containing all the parent nodes of the given node
* @param {HTMLElement} $el - DOM Element
* @return {array} parent nodes
* @param {object} node
* @return {array} parent nodes
*/
const getParents = $el => {
export function getParents(elem) {
// Set up a parent array
let parents = []
let parents = [];
// Push each parent element to the array
for (; $el && $el !== document; $el = $el.parentNode) {
parents.push($el)
for ( ; elem && elem !== document; elem = elem.parentNode ) {
parents.push(elem);
}
// Return our parent array
return parents
return parents;
}
// https://gomakethings.com/how-to-get-the-closest-parent-element-with-a-matching-selector-using-vanilla-javascript/
export function queryClosestParent(elem, selector) {
export {
escapeHtml,
unescapeHtml,
getNodeData,
getData,
getParents,
}
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Get the closest matching element
for ( ; elem && elem !== document; elem = elem.parentNode ) {
if ( elem.matches( selector ) ) return elem;
}
return null;
};

View File

@@ -1,41 +1,18 @@
import { CSS_CLASS } from '../config'
const LAZY_LOADED_IMAGES = []
/**
* Get an image meta data
*
* @param {HTMLImageElement} $img - The image element.
* @return {object} The given image meta data
*/
const getImageMetadata = $img => ({
url: $img.src,
width: $img.naturalWidth,
height: $img.naturalHeight,
ratio: $img.naturalWidth / $img.naturalHeight,
})
/**
* Load the given image.
*
* @param {string} url - The URI to lazy load into $el.
* @param {object} options - An object of options
* @return {void}
*/
const loadImage = (url, options = {}) => {
export function loadImage(url, options = {}) {
return new Promise((resolve, reject) => {
const $img = new Image()
const $img = new Image();
if (options.crossOrigin) {
$img.crossOrigin = options.crossOrigin
$img.crossOrigin = options.crossOrigin;
}
const loadCallback = () => {
resolve({
element: $img,
...getImageMetadata($img),
})
});
}
if($img.decode) {
@@ -46,26 +23,31 @@ const loadImage = (url, options = {}) => {
} else {
$img.onload = loadCallback
$img.onerror = (e) => {
reject(e)
}
reject(e);
};
$img.src = url
}
})
});
}
export function getImageMetadata($img) {
return {
url: $img.src,
width: $img.naturalWidth,
height: $img.naturalHeight,
ratio: $img.naturalWidth / $img.naturalHeight,
};
}
/**
* Lazy load the given image.
*
* @param {HTMLImageElement} $el - The image element.
* @param {?string} url - The URI to lazy load into $el.
* @param {HTMLImageElement} $el - The image element.
* @param {?string} url - The URI to lazy load into $el.
* If falsey, the value of the `data-src` attribute on $el will be used as the URI.
* @param {?function} callback - A function to call when the image is loaded.
* @return {void}
* @param {?function} callback - A function to call when the image is loaded.
*/
const LAZY_LOADED_IMAGES = []
const lazyLoadImage = async ($el, url, callback) => {
export async function lazyLoadImage($el, url, callback) {
let src = url ? url : $el.dataset.src
let loadedImage = LAZY_LOADED_IMAGES.find(image => image.url === src)
@@ -74,7 +56,7 @@ const lazyLoadImage = async ($el, url, callback) => {
loadedImage = await loadImage(src)
if (!loadedImage.url) {
return
return;
}
LAZY_LOADED_IMAGES.push(loadedImage)
@@ -85,28 +67,21 @@ const lazyLoadImage = async ($el, url, callback) => {
}
if ($el.tagName === 'IMG') {
$el.src = loadedImage.url
$el.src = loadedImage.url;
} else {
$el.style.backgroundImage = `url(${loadedImage.url})`
$el.style.backgroundImage = `url(${loadedImage.url})`;
}
requestAnimationFrame(() => {
let lazyParent = $el.closest(`.${CSS_CLASS.LAZY_CONTAINER}`)
let lazyParent = $el.closest('.c-lazy');
if(lazyParent) {
lazyParent.classList.add(CSS_CLASS.LAZY_LOADED)
lazyParent.classList.add('-lazy-loaded')
lazyParent.style.backgroundImage = ''
}
$el.classList.add(CSS_CLASS.LAZY_LOADED)
$el.classList.add('-lazy-loaded')
callback?.()
})
}
export {
getImageMetadata,
loadImage,
lazyLoadImage
}

View File

@@ -1,25 +1,37 @@
/**
* Determines if the argument is object-like.
*
* A value is object-like if it's not `null` and has a `typeof` result of "object".
*
* @param {*} x - The value to be checked.
* @return {boolean}
*/
const toString = Object.prototype.toString;
const arrayLikePattern = /^\[object (?:Array|FileList)\]$/;
const isObject = x => (x && typeof x === 'object')
/**
* Determines if the argument is a function.
*
* @param {*} x - The value to be checked.
* @return {boolean}
*/
const isFunction = x => typeof x === 'function'
export {
isObject,
isFunction
// thanks, http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
export function isArray ( thing ) {
return toString.call( thing ) === '[object Array]';
}
export function isArrayLike ( obj ) {
return arrayLikePattern.test( toString.call( obj ) );
}
export function isEqual ( a, b ) {
if ( a === null && b === null ) {
return true;
}
if ( typeof a === 'object' || typeof b === 'object' ) {
return false;
}
return a === b;
}
// http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric
export function isNumeric ( thing ) {
return !isNaN( parseFloat( thing ) ) && isFinite( thing );
}
export function isObject ( thing ) {
return ( thing && toString.call( thing ) === '[object Object]' );
}
export function isFunction( thing ) {
const getType = {};
return thing && getType.toString.call(thing) === '[object Function]';
}

View File

@@ -1,54 +1,3 @@
/**
* Clamp value
* @param {number} min - start value
* @param {number} max - end value
* @param {number} a - alpha value
* @return {number} clamped value
*/
const clamp = (min = 0, max = 1, a) => Math.min(max, Math.max(min, a))
/**
* Calculate lerp
* @param {number} x - start value
* @param {number} y - end value
* @param {number} a - alpha value
* @return {number} lerp value
*/
const lerp = (x, y, a) => x * (1 - a) + y * a
/**
* Calculate inverted lerp
* @param {number} x - start value
* @param {number} y - end value
* @param {number} a - alpha value
* @return {number} inverted lerp value
*/
const invlerp = (x, y, a) => clamp((a - x)/(y - x))
/**
* Round number to the specified precision.
*
* This function is necessary because `Number.prototype.toPrecision()`
* and `Number.prototype.toFixed()`
*
* @param {number} number - The floating point number to round.
* @param {number} [precision] - The number of digits to appear after the decimal point.
* @return {number} The rounded number.
*/
const roundNumber = (number, precision = 2) => {
return Number.parseFloat(number.toPrecision(precision));
}
export {
clamp,
lerp,
invlerp,
roundNumber
export function lerp(start, end, amt){
return (1 - amt) * start + amt * end
}

View File

@@ -1,78 +0,0 @@
/**
* Creates a debounced function.
*
* A debounced function delays invoking `callback` until after
* `delay` milliseconds have elapsed since the last time the
* debounced function was invoked.
*
* Useful for behaviour that should only happen _before_ or
* _after_ an event has stopped occurring.
*
* @template {function} T
*
* @param {T} callback - The function to debounce.
* @param {number} delay - The number of milliseconds to wait.
* @param {boolean} [immediate] -
* If `true`, `callback` is invoked before `delay`.
* If `false`, `callback` is invoked after `delay`.
* @return {function<T>} The new debounced function.
*/
const debounce = (callback, delay, immediate = false) => {
let timeout = null
return (...args) => {
clearTimeout(timeout)
const later = () => {
timeout = null
if (!immediate) {
callback(...args)
}
}
if (immediate && !timeout) {
callback(...args)
}
timeout = setTimeout(later, delay)
}
}
/**
* Creates a throttled function.
*
* A throttled function invokes `callback` at most once per every
* `delay` milliseconds.
*
* Useful for rate-limiting an event that occurs in quick succession.
*
* @template {function} T
*
* @param {T} callback - The function to throttle.
* @param {number} delay - The number of milliseconds to wait.
* @return {function<T>} The new throttled function.
*/
const throttle = (callback, delay) => {
let timeout = false
return (...args) => {
if (!timeout) {
timeout = true
callback(...args)
setTimeout(() => {
timeout = false
}, delay)
}
}
}
export {
debounce,
throttle
}

View File

@@ -1,35 +1,21 @@
/**
* Get translate function
* @param {HTMLElement} $el - DOM Element
* @return {number|object} translate value
*/
const getTranslate = $el => {
if(!window.getComputedStyle) {
return
}
let translate
const style = getComputedStyle($el)
const transform = style.msTransform || style.webkitTransform || style.MozTransform || style.OTransform || style.transform
const matrix3D = transform.match(/^matrix3d\((.+)\)$/)
if(matrix3D) {
translate = parseFloat(matrix3D[1].split(', ')[13])
} else {
const matrix = transform.match(/^matrix\((.+)\)$/)
translate = {
x: matrix ? parseFloat(matrix[1].split(', ')[4]) : 0
y: matrix ? parseFloat(matrix[1].split(', ')[5]) : 0
}
}
return translate
export function transform(el, transformValue){
el.style.webkitTransform = transformValue;
el.style.msTransform = transformValue;
el.style.transform = transformValue;
}
export function getTranslate(el){
const translate = {}
if(!window.getComputedStyle) return;
export {
transform,
getTranslate
const style = getComputedStyle(el);
const transform = style.transform || style.webkitTransform || style.mozTransform;
let mat = transform.match(/^matrix3d\((.+)\)$/);
if(mat) return parseFloat(mat[1].split(', ')[13]);
mat = transform.match(/^matrix\((.+)\)$/);
translate.x = mat ? parseFloat(mat[1].split(', ')[4]) : 0;
translate.y = mat ? parseFloat(mat[1].split(', ')[5]) : 0;
return translate;
}

View File

@@ -1,118 +1,129 @@
/**
* The `PageVisibility` interface provides support for dispatching
* a custom event derived from the value of {@see document.visibilityState}
* when the "visibilitychange" event is fired.
*
* The custom events are:
*
* - "visibilityhidden" representing the "hidden" visibility state.
* - "visibilityvisible" representing the "visibile" visibility state.
*
* Example:
*
* ```js
* import pageVisibility from './utils/visibility.js';
*
* pageVisibility.enableCustomEvents();
*
* document.addEventListener('visibilityhidden', () => videoElement.pause());
* ```
*
* The dispatched event object is the same from "visibilitychange"
* and renamed according to the visibility state.
*
* The `PageVisibility` interface does not manage the attachment/detachment
* of event listeners on the custom event types.
*
* Further reading:
*
* - {@link https://www.w3.org/TR/page-visibility/ W3 Specification}
* - {@link https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API MDN Web Docs}
*/
export default new class PageVisibility {
/**
* Checks if the "visibilitychange" event listener has been registered.
*
* @return {boolean} Returns `false` if the event listener is not registered,
* otherwise returns `true`.
*/
get isEnabled() {
return isVisibilityChangeObserved;
import { isFunction } from './is';
import { arrayContains, findByKeyValue, removeFromArray } from './array';
import { $document, $window, $html, $body } from './environment';
const CALLBACKS = {
hidden: [],
visible: []
};
const ACTIONS = [
'addCallback',
'removeCallback'
];
const STATES = [
'visible',
'hidden'
];
const PREFIX = 'v-';
let UUID = 0;
// Main event
$document.on('visibilitychange', function(event) {
if (document.hidden) {
onDocumentChange('hidden');
} else {
onDocumentChange('visible');
}
});
/**
* Removes the "visibilitychange" event listener.
*
* @return {boolean} Returns `false` if the event listener was already unregistered,
* otherwise returns `true`.
*/
disableCustomEvents() {
if (isVisibilityChangeObserved) {
isVisibilityChangeObserved = false;
document.removeEventListener('visibilitychange', handleCustomVisibilityChange);
return true;
}
/**
* Add a callback
* @param {string} state
* @param {function} callback
* @return {string} ident
*/
function addCallback (state, options) {
let callback = options.callback || '';
if (!isFunction(callback)) {
console.warn('Callback is not a function');
return false;
}
/**
* Registers the "visibilitychange" event listener.
*
* @return {boolean} Returns `false` if the event listener was already registered,
* otherwise returns `true`.
*/
enableCustomEvents() {
if (!isVisibilityChangeObserved) {
isVisibilityChangeObserved = true;
document.addEventListener('visibilitychange', handleCustomVisibilityChange);
return true;
}
let ident = PREFIX + UUID++;
CALLBACKS[state].push({
ident: ident,
callback: callback
});
return ident;
}
/**
* Remove a callback
* @param {string} state Visible or hidden
* @param {string} ident Unique identifier
* @return {boolean} If operation was a success
*/
function removeCallback (state, options) {
let ident = options.ident || '';
if (typeof(ident) === 'undefined' || ident === '') {
console.warn('Need ident to remove callback');
return false;
}
let index = findByKeyValue(CALLBACKS[state], 'ident', ident)[0];
// console.log(ident)
// console.log(CALLBACKS[state])
if (typeof(index) !== 'undefined') {
removeFromArray(CALLBACKS[state], index);
return true;
} else {
console.warn('Callback could not be found');
return false;
}
}
/**
* Tracks whether custom visibility event types
* are available (`true`) or not (`false`).
*
* @type {boolean}
* When document state changes, trigger callbacks
* @param {string} state Visible or hidden
*/
let isVisibilityChangeObserved = false;
function onDocumentChange (state) {
let callbackArray = CALLBACKS[state];
let i = 0;
let len = callbackArray.length;
/**
* Dispatches a custom visibility event at the document derived
* from the value of {@see document.visibilityState}.
*
* @listens document#visibilitychange
*
* @fires PageVisibility#visibilityhidden
* @fires PageVisibility#visibilityvisible
*
* @param {Event} event
* @return {void}
*/
function handleCustomVisibilityChange(event) {
document.dispatchEvent(new CustomEvent(`visibility${document.visibilityState}`, {
detail: {
cause: event
}
}));
for (; i < len; i++) {
callbackArray[i].callback();
}
}
/**
* The "visibilityhidden" eveent is fired at the document when the contents
* of its tab have become hidden.
*
* @event PageVisibility#visibilityhidden
* @type {Event}
* Public facing API for adding and removing callbacks
* @param {object} options Options
* @return {boolean|integer} Unique identifier for the callback or boolean indicating success or failure
*/
function visibilityApi (options) {
let action = options.action || '';
let state = options.state || '';
let ret;
/**
* The "visibilityvisible" eveent is fired at the document when the contents
* of its tab have become visible.
*
* @event PageVisibility#visibilityvisible
* @type {Event}
*/
// Type and value checking
if (!arrayContains(ACTIONS, action)) {
console.warn('Action does not exist');
return false;
}
if (!arrayContains(STATES, state)) {
console.warn('State does not exist');
return false;
}
// @todo Magic call function pls
if (action === 'addCallback') {
ret = addCallback(state, options);
} else if (action === 'removeCallback') {
ret = removeCallback(state, options);
}
return ret;
}
export { visibilityApi };

View File

@@ -1,7 +1,3 @@
// ==========================================================================
// Components / Buttons
// ==========================================================================
.c-button {
padding: rem(15px) rem(20px);
background-color: lightgray;

View File

@@ -1,7 +1,6 @@
// ==========================================================================
// Components / Form
// Form
// ==========================================================================
.c-form {
}
@@ -13,7 +12,6 @@
// Label
// ==========================================================================
.c-form_label {
display: block;
margin-bottom: rem(10px);
@@ -21,13 +19,12 @@
// Input
// ==========================================================================
$input-icon-color: 424242; // No #
.c-form_input {
padding: rem(10px);
border: 1px solid lightgray;
background-color: colorCode(lightest);
background-color: white;
&:hover {
border-color: darkgray;
@@ -44,7 +41,6 @@ $input-icon-color: 424242; // No #
// Checkbox
// ==========================================================================
$checkbox: rem(18px);
$checkbox-icon-color: $input-icon-color;
@@ -63,7 +59,7 @@ $checkbox-icon-color: $input-icon-color;
top: 50%;
left: 0;
display: inline-block;
margin-top: math.div(-$checkbox, 2);
margin-top: (-$checkbox / 2);
padding: 0;
width: $checkbox;
height: $checkbox;
@@ -71,7 +67,7 @@ $checkbox-icon-color: $input-icon-color;
}
&::before {
background-color: colorCode(lightest);
background-color: $white;
border: 1px solid lightgray;
}
@@ -112,7 +108,6 @@ $checkbox-icon-color: $input-icon-color;
// Radio
// ==========================================================================
$radio-icon-color: $input-icon-color;
.c-form_radioLabel {
@@ -134,7 +129,6 @@ $radio-icon-color: $input-icon-color;
// Select
// =============================================================================
$select-icon: rem(40px);
$select-icon-color: $input-icon-color;
@@ -169,7 +163,6 @@ $select-icon-color: $input-icon-color;
// Textarea
// =============================================================================
.c-form_textarea {
@extend .c-form_input;

View File

@@ -1,79 +0,0 @@
// ==========================================================================
// Components / Headings
// ==========================================================================
// Font sizes
// ==========================================================================
:root {
// Default
--font-size-h1: #{responsive-value(38px, 90px, $from-xl)};
--font-size-h2: #{responsive-value(34px, 72px, $from-xl)};
--font-size-h3: #{responsive-value(28px, 54px, $from-xl)};
--font-size-h4: #{responsive-value(24px, 40px, $from-xl)};
--font-size-h5: #{responsive-value(20px, 30px, $from-xl)};
--font-size-h6: #{responsive-value(18px, 23px, $from-xl)};
}
// Mixins
// ==========================================================================
@mixin heading {
font-family: ff('sans');
font-weight: $font-weight-medium;
}
@mixin heading-h1 {
font-size: var(--font-size-h1);
line-height: 1.1;
}
@mixin heading-h2 {
font-size: var(--font-size-h2);
line-height: 1.1;
}
@mixin heading-h3 {
font-size: var(--font-size-h3);
line-height: 1.1;
}
@mixin heading-h4 {
font-size: var(--font-size-h4);
line-height: 1.2;
}
@mixin heading-h5 {
font-size: var(--font-size-h5);
line-height: 1.2;
}
@mixin heading-h6 {
font-size: var(--font-size-h6);
line-height: 1.4;
}
// Styles
// ==========================================================================
.c-heading {
@include heading;
&.-h1 {
@include heading-h1;
}
&.-h2 {
@include heading-h2;
}
&.-h3 {
@include heading-h3;
}
&.-h4 {
@include heading-h4;
}
&.-h5 {
@include heading-h5;
}
&.-h6 {
@include heading-h6;
}
}

View File

@@ -1,7 +1,3 @@
// ==========================================================================
// Components / Scrollbar
// ==========================================================================
.c-scrollbar {
position: absolute;
right: 0;
@@ -9,7 +5,7 @@
width: 11px;
height: 100vh;
transform-origin: center right;
transition: transform t(normal), opacity t(normal);
transition: transform 0.3s, opacity 0.3s;
opacity: 0;
&:hover {
@@ -25,7 +21,7 @@
position: absolute;
top: 0;
right: 0;
background-color: colorCode(darkest);
background-color: black;
opacity: 0.5;
width: 7px;
border-radius: 10px;

View File

@@ -1,53 +0,0 @@
// ==========================================================================
// Components / Texts
// ==========================================================================
// Font sizes
// ==========================================================================
:root {
--font-size-body-regular: #{responsive-value(15px, 17px, $from-lg)};
--font-size-body-medium: #{responsive-value(18px, 23px, $from-lg)};
--font-size-body-small: #{responsive-value(13px, 16px, $from-lg)};
}
// Mixins
// ==========================================================================
@mixin text {
font-family: ff('sans');
}
@mixin body-regular {
font-size: var(--font-size-body-regular);
font-weight: $font-weight-normal;
line-height: 1.2;
}
@mixin body-medium {
font-size: var(--font-size-body-medium);
font-weight: $font-weight-normal;
line-height: 1.2;
}
@mixin body-small {
font-size: var(--font-size-body-small);
font-weight: $font-weight-normal;
line-height: 1.2;
}
// Styles
// ==========================================================================
.c-text {
@include text;
&.-body-regular {
@include body-regular;
}
&.-body-medium {
@include body-medium;
}
&.-body-small {
@include body-small;
}
}

View File

@@ -1,5 +1 @@
// ==========================================================================
// Critical CSS
// ==========================================================================
$assets-path: "assets/";

View File

@@ -1,63 +0,0 @@
// ==========================================================================
// Elements / Document
// ==========================================================================
//
// Simple page-level setup.
//
// 1. Includes fonts
// 2. Ensure the page always fills at least the entire height of the viewport.
// 3. Set the default `font-size` and `line-height` for the entire project,
// sourced from our default variables.
@include font-faces($font-faces, $font-dir); // [1]
html {
min-height: 100%; // [2]
line-height: $line-height; // [3]
font-family: ff("sans");
color: $font-color;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: $to-sm) {
font-size: $font-size - 2px;
}
@media (min-width: $from-sm) and (max-width: $to-lg) {
font-size: $font-size - 1px;
}
@media (min-width: $from-lg) and (max-width: $to-2xl) {
font-size: $font-size;
}
@media (min-width: $from-2xl) and (max-width: $to-3xl) {
font-size: $font-size + 1px;
}
@media (min-width: $from-3xl) {
font-size: $font-size + 2px;
}
&.is-loading {
cursor: wait;
}
}
body {
}
::selection {
background-color: $color-selection-background;
color: $color-selection-text;
text-shadow: none;
}
a {
color: $color-link;
@include u-hocus {
color: $color-link-hover;
}
}

View File

@@ -0,0 +1,36 @@
// ==========================================================================
// Base / Fonts
// ==========================================================================
// @include font-face(
// $font-foobar,
// "fonts/Foobar/Regular"
// ) {
// font-style: normal;
// font-weight: 400;
// }
// @include font-face(
// $font-foobar,
// "fonts/Foobar/Medium"
// ) {
// font-style: normal;
// font-weight: 500;
// }
// @include font-face(
// $font-foobar,
// "fonts/Foobar/Semibold"
// ) {
// font-style: normal;
// font-weight: 600;
// }
// @include font-face(
// $font-bazqux,
// "fonts/Bazqux/Regular",
// ("eot", "woff2", "woff", "ttf")
// ) {
// font-style: normal;
// font-weight: 400;
// }

View File

@@ -1,124 +0,0 @@
// ==========================================================================
// Elements / Normalize
// ==========================================================================
// Modern CSS Normalize
// Based on the reset by Andy.set with some tweaks.
// Original by Andy.set: https://piccalil.li/blog/a-more-modern-css-reset/
// Review by Chris collier: https://chriscoyier.net/2023/10/03/being-picky-about-a-css-reset-for-fun-pleasure/
// Box sizing rules
*,
*:after,
*:before {
box-sizing: border-box;
}
// Prevent font size inflation
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
// Remove default margin in favour of better control in authored CSS
p,
h1,
h2,
h3,
h4,
h5,
h6,
dl,
dd,
figure,
blockquote {
margin-block: unset;
}
// Remove list styles on ul, ol elements with a class, which suggests default styling will be removed
ul[class],
ol[class] {
margin: 0;
padding: 0;
list-style: none;
}
// Set core defaults
html {
line-height: 1.5;
}
body {
margin: unset;
}
// Set shorter line heights on headings and interactive elements
h1,
h2,
h3,
h4,
h5,
h6,
input,
label,
button {
line-height: 1.1;
}
// Balance text wrapping on headings
h1,
h2,
h3,
h4,
h5,
h6 {
text-wrap: balance;
}
// Remove a elements default styles if they have a class
a[class] {
color: inherit;
text-decoration: none;
}
// Make assets easier to work with
img,
svg,
canvas,
picture {
display: block;
max-inline-size: 100%;
block-size: auto;
}
// Inherit fonts for inputs and buttons
input,
button,
select,
textarea {
font: inherit;
}
// Make sure textareas without a rows attribute are not tiny
textarea:not([rows]) {
min-height: 10em;
}
// Anything that has been anchored to should have extra scroll margin
:target {
scroll-margin-block: 1rlh;
}
// Reduced mootion preference
@media (prefers-reduced-motion: reduce) {
*,
*:after,
*:before {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -0,0 +1,90 @@
// ==========================================================================
// Base / Page
// ==========================================================================
:root {
// Font sizes
--font-size-h1: #{rem(36px)};
--font-size-h2: #{rem(28px)};
--font-size-h3: #{rem(24px)};
--font-size-h4: #{rem(20px)};
--font-size-h5: #{rem(18px)};
--font-size-h6: #{rem(16px)};
}
//
// Simple page-level setup.
//
// 1. Set the default `font-size` and `line-height` for the entire project,
// sourced from our default variables.
// 2. Ensure the page always fills at least the entire height of the viewport.
//
html {
min-height: 100%; /* [2] */
color: $color;
font-family: $font-family;
line-height: $line-height; /* [1] */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: $to-small) {
font-size: $font-size - 2px;
}
@media (min-width: $from-small) and (max-width: $to-medium) {
font-size: $font-size - 2px;
}
@media (min-width: $from-medium) and (max-width: $to-large) {
font-size: $font-size - 1px;
}
@media (min-width: $from-large) and (max-width: $to-huge) {
font-size: $font-size; /* [1] */
}
@media (min-width: $from-huge) and (max-width: $to-gigantic) {
font-size: $font-size + 1px;
}
@media (min-width: $from-gigantic) and (max-width: $to-colossal) {
font-size: $font-size + 2px;
}
@media (min-width: $from-colossal) {
font-size: $font-size + 4px;
}
&.is-loading {
cursor: wait;
}
&.has-scroll-smooth {
overflow: hidden;
}
&.has-scroll-dragging {
user-select: none;
}
}
body {
.has-scroll-smooth & {
overflow: hidden;
}
}
::selection {
background-color: $selection-background-color;
color: $selection-text-color;
text-shadow: none;
}
a {
color: $link-color;
@include u-hocus {
color: $link-hover-color;
}
}

View File

@@ -0,0 +1,35 @@
// ==========================================================================
// Generic / Buttons
// ==========================================================================
//
// 1. Allow us to style box model properties.
// 2. Fixes odd inner spacing in IE7.
// 3. Reset/normalize some styles.
// 4. Line different sized buttons up a little nicer.
// 5. Make buttons inherit font styles (often necessary when styling `input`s as buttons).
// 6. Force all button-styled elements to appear clickable.
//
button,
.c-button {
@include u-hocus {
text-decoration: none;
}
display: inline-block; /* [1] */
overflow: visible; /* [2] */
margin: 0; /* [3] */
padding: 0;
outline: 0;
border: 0;
background: none transparent;
color: inherit;
vertical-align: middle; /* [4] */
text-align: center; /* [3] */
text-decoration: none;
text-transform: none;
font: inherit; /* [5] */
line-height: normal;
cursor: pointer; /* [6] */
user-select: none;
}

View File

@@ -0,0 +1,44 @@
// ==========================================================================
// Generic / Forms
// ==========================================================================
input,
select,
textarea {
display: block;
margin: 0;
padding: 0;
width: 100%;
outline: 0;
border: 0;
border-radius: 0;
background: none transparent;
color: inherit;
font: inherit;
line-height: normal;
appearance: none;
}
select {
text-transform: none;
&::-ms-expand {
display: none;
}
&::-ms-value {
background: none;
color: inherit;
}
// Remove Firefox :focus dotted outline, breaks color inherit
// &:-moz-focusring {
// color: transparent;
// text-shadow: 0 0 0 #000000; // Text :focus color
// }
}
textarea {
overflow: auto;
resize: vertical;
}

View File

@@ -0,0 +1,89 @@
// ==========================================================================
// Generic
// ==========================================================================
html {
box-sizing: border-box;
}
//
// Add the correct display in IE 10-.
// 1. Add the correct display in IE.
//
template, /* [1] */
[hidden] {
display: none;
}
*,
:before,
:after {
box-sizing: inherit;
}
address {
font-style: inherit;
}
dfn,
cite,
em,
i {
font-style: italic;
}
b,
strong {
font-weight: $bold;
}
a {
text-decoration: none;
svg {
pointer-events: none;
}
}
ul,
ol {
margin: 0;
padding: 0;
list-style: none;
}
p,
figure {
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
/**
* 1. Single taps should be dispatched immediately on clickable elements
*/
a, area, button, input, label, select, textarea, [tabindex] {
-ms-touch-action: manipulation; /* [1] */
touch-action: manipulation;
}
[hreflang] > abbr[title] {
text-decoration: none;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
hr {
display: block;
margin: 1em 0;
padding: 0;
height: 1px;
border: 0;
border-top: 1px solid #CCCCCC;
}

View File

@@ -0,0 +1,58 @@
// ==========================================================================
// Generic / Media
// ==========================================================================
//
// 1. Setting `vertical-align` removes the whitespace that appears under `img`
// elements when they are dropped into a page as-is. Safer alternative to
// using `display: block;`.
//
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: middle; /* [1] */
}
//
// Add the correct display in iOS 4-7.
//
audio:not([controls]) {
display: none;
height: 0;
}
//
// 2. Fluid media for responsive purposes.
//
img,
svg {
max-width: 100%; /* [2] */
height: auto;
//
// 4. If a `width` and/or `height` attribute have been explicitly defined, lets
// not make the image fluid.
//
&[width], /* [4] */
&[height] {
/* [4] */
max-width: none;
}
}
//
// 4. Offset `alt` text from surrounding copy.
//
img {
font-style: italic; /* [4] */
}
//
// 5. SVG elements should fallback to their surrounding text color.
//
svg {
fill: currentColor; /* [5] */
}

View File

@@ -2,70 +2,69 @@
// Main
// ==========================================================================
// Modules
// ==========================================================================
@use "sass:math";
// Tools
// ==========================================================================
@import "tools/maths";
@import "tools/functions";
@import "tools/mixins";
// @import "tools/layout";
// @import "tools/widths";
// @import "tools/family";
// Settings
// ==========================================================================
@import "settings/config";
@import "settings/config.breakpoints";
@import "settings/config.colors";
@import "settings/config.eases";
@import "settings/config.fonts";
@import "settings/config.spacings";
@import "settings/config.speeds";
@import "settings/config.zindexes";
@import "settings/config.variables";
@import "settings/config.colors";
@import "settings/config";
// Vendors
// ==========================================================================
@import "../../node_modules/locomotive-scroll/dist/locomotive-scroll";
// Tools
// ==========================================================================
@import "tools/functions";
@import "tools/mixins";
@import "tools/fonts";
@import "tools/layout";
@import "tools/widths";
// @import "tools/family";
// Generic
// ==========================================================================
@import "node_modules/normalize.css/normalize";
@import "generic/generic";
@import "generic/media";
@import "generic/form";
@import "generic/button";
// Elements
// ==========================================================================
@import "elements/fonts";
@import "elements/page";
@import "elements/normalize";
@import "elements/document";
// Typography
// ==========================================================================
@import "typography/headings";
@import "typography/wysiwyg";
// Objects
// ==========================================================================
@import "objects/scroll";
@import "objects/container";
@import "objects/ratio";
@import "objects/icons";
@import "objects/grid";
// @import "objects/layout";
@import "objects/layout";
// @import "objects/crop";
// @import "objects/table";
// Vendors
// ==========================================================================
// @import "vendors/vendor";
// Components
// ==========================================================================
@import "components/heading";
@import "components/text";
@import "components/scrollbar";
@import "components/button";
@import "components/form";
// Templates
// ==========================================================================
// @import "templates/template";
// Utilities
// ==========================================================================
@import "utilities/ratio";
@import "utilities/grid-column";
// @import "utilities/widths";
@import "utilities/widths";
// @import "utilities/align";
// @import "utilities/helpers";
// @import "utilities/states";
@import "utilities/spacing";
// @import "utilities/spacing";
// @import "utilities/print";

View File

@@ -2,6 +2,7 @@
// Objects / Container
// ==========================================================================
//
// Page-level constraining and wrapping elements.
//
// > In programming languages the word *container* is generally used for structures
@@ -9,10 +10,12 @@
// > A *wrapper* instead is something that wraps around a single object to provide
// more functionalities and interfaces to it.
// @link http://stackoverflow.com/a/13202141/140357
//
.o-container {
margin-right: auto;
margin-left: auto;
padding-left: var(--grid-margin);
padding-right: var(--grid-margin);
padding-right: rem($padding);
padding-left: rem($padding);
max-width: rem($container-width + ($padding * 2));
}

View File

@@ -0,0 +1,88 @@
// ==========================================================================
// Objects / Crop
// ==========================================================================
//
// @link https://github.com/inuitcss/inuitcss/blob/19d0c7e/objects/_objects.crop.scss
//
// A list of cropping ratios that get generated as modifier classes.
$crop-ratios: (
(2:1),
(4:3),
(16:9),
) !default;
/**
* Provide a cropping container in order to display media (usually images)
* cropped to certain ratios.
*
* 1. Set up a positioning context in which the image can sit.
* 2. This is the crucial part: where the cropping happens.
*/
.o-crop {
position: relative; /* [1] */
display: block;
overflow: hidden; /* [2] */
}
/**
* Apply this class to the content (usually `img`) that needs cropping.
*
* 1. Images default positioning is top-left in the cropping box.
* 2. Make sure the media doesnt stop itself too soon.
*/
.o-crop_content {
position: absolute;
top: 0; /* [1] */
left: 0; /* [1] */
max-width: none; /* [2] */
/**
* We can position the media in different locations within the cropping area.
*/
&.-right {
right: 0;
left: auto;
}
&.-bottom {
top: auto;
bottom: 0;
}
&.-center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
/* stylelint-disable */
//
// Generate a series of crop classes to be used like so:
//
// @example
// <div class="o-crop -16:9">
//
//
.o-crop {
@each $crop in $crop-ratios {
@each $antecedent, $consequent in $crop {
@if (type-of($antecedent) != number) {
@error "`#{$antecedent}` needs to be a number."
}
@if (type-of($consequent) != number) {
@error "`#{$consequent}` needs to be a number."
}
&.-#{$antecedent}\:#{$consequent} {
padding-bottom: ($consequent/$antecedent) * 100%;
}
}
}
}
/* stylelint-enable */

View File

@@ -1,178 +0,0 @@
// ==========================================================================
// Grid helper
// ==========================================================================
// Help: https://css-tricks.com/snippets/css/complete-guide-grid/
//
/**
* Usage:
*
* ```html
* <div class="o-grid -col-4 -col-12@from-medium -gutters">
* <div class="o-grid_item u-gc-1/2 u-gc-3/9@from-medium">
* <p>Hello</p>
* </div>
* <div class="o-grid_item u-gc-3/4 u-gc-9/13@from-medium">
* <p>Hello</p>
* </div>
* </div>
* ```
*/
.o-grid {
display: grid;
width: 100%;
&:is(ul, ol) {
margin: 0;
padding: 0;
list-style: none;
}
// ==========================================================================
// Cols
// ==========================================================================
// Responsive grid columns based on `--grid-columns`
&.-cols {
grid-template-columns: repeat(var(--grid-columns), 1fr);
}
&.-col-#{$base-column-nb} {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
&.-col-4 {
grid-template-columns: repeat(4, 1fr);
}
&.-col-#{$base-column-nb}\@from-md {
@media (min-width: $from-md) {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
}
// ==========================================================================
// Gutters
// ==========================================================================
// Gutters rows and columns
&.-gutters {
gap: var(--grid-gutter);
column-gap: var(--grid-gutter);
}
// ==========================================================================
// Modifiers
// ==========================================================================
&.-full-height {
height: 100%;
}
// ==========================================================================
// Aligns
// ==========================================================================
// ==========================================================================
// Items inside cells
//
&.-top-items {
align-items: start;
}
&.-right-items {
justify-items: end;
}
&.-bottom-items {
align-items: end;
}
&.-left-items {
justify-items: start;
}
&.-center-items {
align-items: center;
justify-items: center;
}
&.-center-items-x {
justify-items: center;
}
&.-center-items-y {
align-items: center;
}
&.-stretch-items {
align-items: stretch;
justify-items: stretch;
}
// ==========================================================================
// Cells
//
&.-top-cells {
align-content: start;
}
&.-right-cells {
justify-content: end;
}
&.-bottom-cells {
align-content: end;
}
&.-left-cells {
justify-content: start;
}
&.-center-cells {
align-content: center;
justify-content: center;
}
&.-center-cells-x {
justify-content: center;
}
&.-center-cells-y {
align-content: center;
}
&.-stretch-cells {
align-content: stretch;
justify-content: stretch;
}
&.-space-around-cells {
align-content: space-around;
justify-content: space-around;
}
&.-space-around-cells-x {
justify-content: space-around;
}
&.-space-around-cells-y {
align-content: space-around;
}
&.-space-between-cells {
justify-content: space-between;
align-content: space-between;
}
&.-space-between-cells-x {
justify-content: space-between;
}
&.-space-between-cells-y {
align-content: space-between;
}
&.-space-evenly-cells {
justify-content: space-evenly;
align-content: space-evenly;
}
&.-space-evenly-cells-x {
justify-content: space-evenly;
}
&.-space-evenly-cells-y {
align-content: space-evenly;
}
}
// ==========================================================================
// Grid item
// ==========================================================================
// By default, a grid item takes full width of its parent.
//
.o-grid_item {
grid-column-start: var(--gc-start, 1);
grid-column-end: var(--gc-end, -1);
&.-align-end {
align-self: end;
}
}

View File

@@ -1,58 +0,0 @@
// ==========================================================================
// Objects / SVG Icons
// ==========================================================================
// Markup
//
// 1. If icon is accessible and has a title
// 2. If icon is decorative
//
// <i class="o-icon ${modifier}">
// <svg
// class="svg-${icon-name}"
// xmlns="http://www.w3.org/2000/svg"
// role="img" [1]
// aria-hidden="true" [2]
// focusable="false" [2]
// aria-labelledby="${id}" [1]
// >
// <title id="${id}"> [1]
// Locomotive
// </title>
// <use xlink:href="assets/images/sprite.svg#${icon-name}" xmlns:xlink="http://www.w3.org/1999/xlink"/>
// </svg>
// </i>
// Global styles for icones
// ==========================================================================
.o-icon {
display: inline-block;
vertical-align: middle;
svg {
--icon-height: calc(var(--icon-width) * math.div(1, (var(--icon-ratio))));
display: block;
width: var(--icon-width);
height: var(--icon-height);
fill: currentColor;
}
}
// SVG sizes
// ==========================================================================
// // Logo
// .svg-logo {
// --icon-width: #{rem(100px)};
// --icon-ratio: math.div(20, 30); // width/height based on svg viewBox
// // Sizes
// .o-icon.-big & {
// --icon-width: #{rem(200px)};
// }
// }

View File

@@ -2,41 +2,41 @@
// Objects / Layout
// ==========================================================================
////
/// Grid-like layout system.
///
/// The layout object provides us with a column-style layout system. This file
/// contains the basic structural elements, but classes should be complemented
/// with width utilities, for example:
///
/// @example
/// <div class="o-layout">
/// <div class="o-layout_item u-1/1 u-1/3@medium">
/// </div>
/// <div class="o-layout_item u-1/2 u-1/3@medium">
/// </div>
/// <div class="o-layout_item u-1/2 u-1/3@medium">
/// </div>
/// </div>
///
/// We can also manipulate entire layout systems by adding a series of modifiers
/// to the `.o-layout` block. For example:
///
/// @example
/// <div class="o-layout -reverse">
///
/// This will reverse the displayed order of the system so that it runs in the
/// opposite order to our source, effectively flipping the system over.
///
/// @example
/// <div class="o-layout -[right|center]">
///
/// This will cause the system to fill up from either the centre or the right
/// hand side. Default behaviour is to fill up the layout system from the left.
///
/// @requires tools/layout
/// @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss
////
//
// Grid-like layout system.
//
// The layout object provides us with a column-style layout system. This file
// contains the basic structural elements, but classes should be complemented
// with width utilities, for example:
//
// @example
// <div class="o-layout">
// <div class="o-layout_item u-1/1 u-1/3@medium">
// </div>
// <div class="o-layout_item u-1/2 u-1/3@medium">
// </div>
// <div class="o-layout_item u-1/2 u-1/3@medium">
// </div>
// </div>
//
// We can also manipulate entire layout systems by adding a series of modifiers
// to the `.o-layout` block. For example:
//
// @example
// <div class="o-layout -reverse">
//
// This will reverse the displayed order of the system so that it runs in the
// opposite order to our source, effectively flipping the system over.
//
// @example
// <div class="o-layout -[right|center]">
//
// This will cause the system to fill up from either the centre or the right
// hand side. Default behaviour is to fill up the layout system from the left.
//
// @requires tools/layout
// @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss
//
.o-layout {
@include o-layout;

View File

@@ -2,13 +2,14 @@
// Objects / Ratio
// ==========================================================================
// Create ratio-bound content blocks, to keep media (e.g. images, videos) in
// their correct aspect ratios.
//
// http://alistapart.com/article/creating-intrinsic-ratios-for-video
//
// 1. Default cropping is a 1:1 ratio (i.e. a perfect square).
/**
* Create ratio-bound content blocks, to keep media (e.g. images, videos) in
* their correct aspect ratios.
*
* http://alistapart.com/article/creating-intrinsic-ratios-for-video
*
* 1. Default cropping is a 1:1 ratio (i.e. a perfect square).
*/
.o-ratio {
position: relative;
display: block;
@@ -16,7 +17,7 @@
&:before {
display: block;
padding-bottom: 100%; // [1]
padding-bottom: 100%; /* [1] */
width: 100%;
content: "";
}

View File

@@ -0,0 +1,3 @@
.o-scroll {
min-height: 100vh;
}

View File

@@ -1,14 +1,14 @@
// ==========================================================================
// Objects / Tables
// ==========================================================================
.o-table {
width: 100%;
// Force all cells within a table to occupy the same width as each other.
//
// @link https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout#Values
/**
* Force all cells within a table to occupy the same width as each other.
*
* @link https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout#Values
*/
&.-fixed {
table-layout: fixed;
}

View File

@@ -1,92 +0,0 @@
// ==========================================================================
// Settings / Config / Breakpoints
// ==========================================================================
// Breakpoints
// ==========================================================================
$breakpoints: (
"2xs": 340px,
"xs": 500px,
"sm": 700px,
"md": 1000px,
"lg": 1200px,
"xl": 1400px,
"2xl": 1600px,
"3xl": 1800px,
"4xl": 2000px,
"5xl": 2400px
);
// Functions
// ==========================================================================
// Creates a min-width or max-width media query expression.
//
// @param {string} $breakpoint The breakpoint.
// @param {string} $type Either "min" or "max".
// @return {string}
@function mq($breakpoint, $type: "min") {
@if not map-has-key($breakpoints, $breakpoint) {
@warn "Unknown media query breakpoint: `#{$breakpoint}`";
}
$value: map-get($breakpoints, $breakpoint);
@if ($type == "min") {
@return "(min-width: #{$value})";
}
@if ($type == "max") {
@return "(max-width: #{$value - 1px})";
}
@error "Unknown media query type: #{$type}";
}
// Creates a min-width media query expression.
//
// @param {string} $breakpoint The breakpoint.
// @return {string}
@function mq-min($breakpoint) {
@return mq($breakpoint, "min");
}
// Creates a max-width media query expression.
//
// @param {string} $breakpoint The breakpoint.
// @return {string}
@function mq-max($breakpoint) {
@return mq($breakpoint, "max");
}
// Creates a min-width and max-width media query expression.
//
// @param {string} $from The min-width breakpoint.
// @param {string} $until The max-width breakpoint.
// @return {string}
@function mq-between($breakpointMin, $breakpointMax) {
@return "#{mq-min($breakpointMin)} and #{mq-max($breakpointMax)}";
}
// Legacy
// ==========================================================================
$from-xs: map-get($breakpoints, "xs") !default;
$to-xs: map-get($breakpoints, "xs") - 1 !default;
$from-sm: map-get($breakpoints, "sm") !default;
$to-sm: map-get($breakpoints, "sm") - 1 !default;
$from-md: map-get($breakpoints, "md") !default;
$to-md: map-get($breakpoints, "md") - 1 !default;
$from-lg: map-get($breakpoints, "lg") !default;
$to-lg: map-get($breakpoints, "lg") - 1 !default;
$from-xl: map-get($breakpoints, "xl") !default;
$to-xl: map-get($breakpoints, "xl") - 1 !default;
$from-2xl: map-get($breakpoints, "2xl") !default;
$to-2xl: map-get($breakpoints, "2xl") - 1 !default;
$from-3xl: map-get($breakpoints, "3xl") !default;
$to-3xl: map-get($breakpoints, "3xl") - 1 !default;

View File

@@ -1,61 +1,25 @@
@use 'sass:color';
// ==========================================================================
// Settings / Config / Colors
// ==========================================================================
// Palette
// ==========================================================================
$colors: (
primary: #3297FD,
lightest: #FFFFFF,
darkest: #000000,
);
// Function
// ==========================================================================
// Returns color code.
//
// ```scss
// .c-box {
// color: colorCode(primary);
// }
// ```
//
// @param {string} $key - The color key in $colors.
// @param {number} $alpha - The alpha for the color value.
// @return {color}
@function colorCode($key, $alpha: 1) {
@if not map-has-key($colors, $key) {
@error "Unknown '#{$key}' in $colors.";
}
@if($alpha < 0 or $alpha > 1) {
@error "Alpha '#{$alpha}' must be in range [0, 1].";
}
$color: map-get($colors, $key);
@return rgba($color, $alpha);
}
// Specifics
// ==========================================================================
// =============================================================================
$white: #FFFFFF;
$black: #000000;
// Specific
// =============================================================================
// Link
$color-link: colorCode(primary);
$color-link-focus: colorCode(primary);
$color-link-hover: color.adjust(colorCode(primary), $lightness: -10%);
$link-color: #1A0DAB;
$link-focus-color: #1A0DAB;
$link-hover-color: darken(#1A0DAB, 10%);
// Selection
$color-selection-text: colorCode(darkest);
$color-selection-background: colorCode(lightest);
$selection-text-color: #3297FD;
$selection-background-color: #FFFFFF;
// Socials
$color-facebook: #3B5998;
$color-instagram: #E1306C;
$color-youtube: #CD201F;
$color-twitter: #1DA1F2;
// Social Colors
// =============================================================================
$facebook-color: #3B5998;
$instagram-color: #E1306C;
$youtube-color: #CD201F;
$twitter-color: #1DA1F2;

View File

@@ -1,78 +1,19 @@
// ==========================================================================
// Settings / Config / Eases
// ==========================================================================
// Eases
// ==========================================================================
$eases: (
// Power 1
"power1.in": cubic-bezier(0.550, 0.085, 0.680, 0.530),
"power1.out": cubic-bezier(0.250, 0.460, 0.450, 0.940),
"power1.inOut": cubic-bezier(0.455, 0.030, 0.515, 0.955),
// Power 2
"power2.in": cubic-bezier(0.550, 0.055, 0.675, 0.190),
"power2.out": cubic-bezier(0.215, 0.610, 0.355, 1.000),
"power2.inOut": cubic-bezier(0.645, 0.045, 0.355, 1.000),
// Power 3
"power3.in": cubic-bezier(0.895, 0.030, 0.685, 0.220),
"power3.out": cubic-bezier(0.165, 0.840, 0.440, 1.000),
"power3.inOut": cubic-bezier(0.770, 0.000, 0.175, 1.000),
// Power 4
"power4.in": cubic-bezier(0.755, 0.050, 0.855, 0.060),
"power4.out": cubic-bezier(0.230, 1.000, 0.320, 1.000),
"power4.inOut": cubic-bezier(0.860, 0.000, 0.070, 1.000),
// Expo
"expo.in": cubic-bezier(0.950, 0.050, 0.795, 0.035),
"expo.out": cubic-bezier(0.190, 1.000, 0.220, 1.000),
"expo.inOut": cubic-bezier(1.000, 0.000, 0.000, 1.000),
// Back
"back.in": cubic-bezier(0.600, -0.280, 0.735, 0.045),
"back.out": cubic-bezier(0.175, 00.885, 0.320, 1.275),
"back.inOut": cubic-bezier(0.680, -0.550, 0.265, 1.550),
// Sine
"sine.in": cubic-bezier(0.470, 0.000, 0.745, 0.715),
"sine.out": cubic-bezier(0.390, 0.575, 0.565, 1.000),
"sine.inOut": cubic-bezier(0.445, 0.050, 0.550, 0.950),
// Circ
"circ.in": cubic-bezier(0.600, 0.040, 0.980, 0.335),
"circ.out": cubic-bezier(0.075, 0.820, 0.165, 1.000),
"circ.inOut": cubic-bezier(0.785, 0.135, 0.150, 0.860),
// Misc
"bounce": cubic-bezier(0.17, 0.67, 0.3, 1.33),
"slow.out": cubic-bezier(.04,1.15,0.4,.99),
"smooth": cubic-bezier(0.380, 0.005, 0.215, 1),
);
// Default value for ease()
$ease-default: "power2.out" !default;
// Function
// ==========================================================================
// Returns ease curve.
//
// ```scss
// .c-box {
// transition-timing-function: ease("power2.out");
// }
// ```
//
// @param {string} $key - The ease key in $eases.
// @return {easing-function}
@function ease($key: $ease-default) {
@if not map-has-key($eases, $key) {
@error "Unknown '#{$key}' in $eases.";
}
@return map-get($eases, $key);
}
$Power1EaseOut: cubic-bezier(0.250, 0.460, 0.450, 0.940);
$Power2EaseOut: cubic-bezier(0.215, 0.610, 0.355, 1.000);
$Power3EaseOut: cubic-bezier(0.165, 0.840, 0.440, 1.000);
$Power4EaseOut: cubic-bezier(0.230, 1.000, 0.320, 1.000);
$Power1EaseIn: cubic-bezier(0.550, 0.085, 0.680, 0.530) ;
$Power2EaseIn: cubic-bezier(0.550, 0.055, 0.675, 0.190);
$Power3EaseIn: cubic-bezier(0.895, 0.030, 0.685, 0.220);
$Power4EaseIn: cubic-bezier(0.755, 0.050, 0.855, 0.060);
$ExpoEaseOut: cubic-bezier(0.190, 1.000, 0.220, 1.000);
$ExpoEaseIn: cubic-bezier(0.950, 0.050, 0.795, 0.035);
$ExpoEaseInOut: cubic-bezier(1.000, 0.000, 0.000, 1.000);
$SineEaseOut: cubic-bezier(0.390, 0.575, 0.565, 1.000);
$SineEaseIn: cubic-bezier(0.470, 0.000, 0.745, 0.715);
$Power1EaseInOut: cubic-bezier(0.455, 0.030, 0.515, 0.955);
$Power2EaseInOut: cubic-bezier(0.645, 0.045, 0.355, 1.000);
$Power3EaseInOut: cubic-bezier(0.770, 0.000, 0.175, 1.000);
$Power4EaseInOut: cubic-bezier(0.860, 0.000, 0.070, 1.000);
$SlowEaseOut: cubic-bezier(.04,1.15,0.4,.99);
$bounce: cubic-bezier(0.17, 0.67, 0.3, 1.33);

View File

@@ -1,98 +0,0 @@
// ==========================================================================
// Settings / Config / Breakpoints
// ==========================================================================
// Font fallbacks (retrieved from systemfontstack.com on 2022-05-31)
// ==========================================================================
$font-fallback-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
$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;
$font-fallback-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
// Typefaces
// ==========================================================================
// List of custom font faces as tuples.
//
// ```
// <font-name> <font-file-basename> <font-weight> <font-style>
// ```
$font-faces: (
("Source Sans", "SourceSans3-Bold", 700, normal),
("Source Sans", "SourceSans3-BoldIt", 700, italic),
("Source Sans", "SourceSans3-Regular", 400, normal),
("Source Sans", "SourceSans3-RegularIt", 400, italic),
);
// Map of font families.
//
// ```
// <font-id>: (<font-name>, <font-fallbacks>)
// ```
$font-families: (
sans: join("Source Sans", $font-fallback-sans, $separator: comma),
);
// Font directory
$font-dir: "../fonts/";
// Functions
// ==========================================================================
// Imports the custom font.
//
// The mixin expects font files to be woff and woff2.
//
// @param {List} $webfont - A custom font to import, as a tuple:
// `<font-name> <font-file-basename> <font-weight> <font-style>`.
// @param {String} $dir - The webfont directory path.
// @output The `@font-face` at-rule specifying the custom font.
@mixin font-face($webfont, $dir) {
@font-face {
font-display: swap;
font-family: nth($webfont, 1);
src: url("#{$dir}#{nth($webfont, 2)}.woff2") format("woff2"),
url("#{$dir}#{nth($webfont, 2)}.woff") format("woff");
font-weight: #{nth($webfont, 3)};
font-style: #{nth($webfont, 4)};
}
}
// Imports the list of custom fonts.
//
// @require {mixin} font-face
//
// @param {List<List>} $webfonts - List of custom fonts to import.
// See `font-face` mixin for details.
// @param {String} $dir - The webfont directory path.
// @output The `@font-face` at-rules specifying the custom fonts.
@mixin font-faces($webfonts, $dir) {
@if (length($webfonts) > 0) {
@if (type-of(nth($webfonts, 1)) == "list") {
@each $webfont in $webfonts {
@include font-face($webfont, $dir);
}
} @else {
@include font-face($webfonts, $dir);
}
}
}
// Retrieves the font family stack for the given font ID.
//
// @require {variable} $font-families - See settings directory.
//
// @param {String} $font-family - The custom font ID.
// @throws Error if the $font-family does not exist.
// @return {List} The font stack.
@function ff($font-family) {
@if not map-has-key($font-families, $font-family) {
@error "No font-family found in $font-families map for `#{$font-family}`.";
}
$value: map-get($font-families, $font-family);
@return $value;
}

View File

@@ -4,42 +4,61 @@
// Context
// =============================================================================
// The current stylesheet context. Available values: frontend, editor.
$context: frontend !default;
// Path is relative to the stylesheets directory.
$assets-path: "../" !default;
// Typefaces
// =============================================================================
$font-sans-serif: sans-serif;
// Typography
// =============================================================================
// Base
$font-size: 16px;
$line-height: math.div(24px, $font-size);
$font-color: colorCode(darkest);
$font-size: 16px;
$line-height: 24px / $font-size;
$font-family: $font-sans-serif;
$color: #222222;
// Weights
$font-weight-light: 300;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-bold: 700;
$light: 300;
$normal: 400;
$medium: 500;
$bold: 700;
// Transition defaults
// Transitions
// =============================================================================
$speed: t(normal);
$easing: ease("power2.out");
$speed: 0.3s;
$easing: $Power2EaseOut;
// Spacing Units
// =============================================================================
$unit: 60px;
$unit-small: 20px;
$vw-viewport: 1440;
$unit: 60px;
$unit-small: 30px;
// Container
// ==========================================================================
$padding: $unit;
$container-width: 2000px;
$padding: $unit;
// Grid
// ==========================================================================
$base-column-nb: 12;
// Breakpoints
// =============================================================================
$from-tiny: 500px !default;
$to-tiny: $from-tiny - 1 !default;
$from-small: 700px !default;
$to-small: $from-small - 1 !default;
$from-medium: 1000px !default;
$to-medium: $from-medium - 1 !default;
$from-large: 1200px !default;
$to-large: $from-large - 1 !default;
$from-big: 1400px !default;
$to-big: $from-big - 1 !default;
$from-huge: 1600px !default;
$to-huge: $from-huge - 1 !default;
$from-enormous: 1800px !default;
$to-enormous: $from-enormous - 1 !default;
$from-gigantic: 2000px !default;
$to-gigantic: $from-gigantic - 1 !default;
$from-colossal: 2400px !default;
$to-colossal: $from-colossal - 1 !default;

View File

@@ -1,69 +0,0 @@
// ==========================================================================
// Settings / Config / Spacings
// ==========================================================================
:root {
--spacing-2xs-mobile: 6;
--spacing-2xs-desktop: 10;
--spacing-xs-mobile: 12;
--spacing-xs-desktop: 16;
--spacing-sm-mobile: 22;
--spacing-sm-desktop: 32;
--spacing-md-mobile: 32;
--spacing-md-desktop: 56;
--spacing-lg-mobile: 48;
--spacing-lg-desktop: 96;
--spacing-xl-mobile: 64;
--spacing-xl-desktop: 128;
--spacing-2xl-mobile: 88;
--spacing-2xl-desktop: 176;
--spacing-3xl-mobile: 122;
--spacing-3xl-desktop: 224;
}
// Spacings
// ==========================================================================
$spacings: (
'gutter': var(--grid-gutter),
'2xs': #{size-clamp('2xs')},
'xs': #{size-clamp('xs')},
'sm': #{size-clamp('sm')},
'md': #{size-clamp('md')},
'lg': #{size-clamp('lg')},
'xl': #{size-clamp('xl')},
'2xl': #{size-clamp('2xl')},
'3xl': #{size-clamp('3xl')},
);
// Function
// ==========================================================================
// Returns spacing.
//
// ```scss
// .c-box {
// margin-top: spacing(gutter);
// }
// ```
//
// @param {string} $key - The spacing key in $spacings.
// @param {number} $multiplier - The multiplier of the spacing value.
// @return {size}
@function spacing($spacing: 'sm', $multiplier: 1) {
@if not map-has-key($spacings, $spacing) {
@error "Unknown master spacing: #{$spacing}";
}
$index: map-get($spacings, $spacing);
@return calc(#{$index} * #{$multiplier});
}

View File

@@ -1,38 +0,0 @@
// ==========================================================================
// Settings / Config / Speeds
// ==========================================================================
// Speeds
// ==========================================================================
$speeds: (
fastest: 0.1s,
faster: 0.15s,
fast: 0.25s,
normal: 0.3s,
slow: 0.5s,
slower: 0.75s,
slowest: 1s,
);
// Function
// ==========================================================================
// Returns timing.
//
// ```scss
// .c-box {
// transition-duration: speed(slow);
// }
// ```
//
// @param {string} $key - The speed key in $speeds.
// @return {duration}
@function speed($key: "normal") {
@if not map-has-key($speeds, $key) {
@error "Unknown '#{$key}' in $speeds.";
}
@return map-get($speeds, $key);
}

View File

@@ -1,20 +0,0 @@
// ==========================================================================
// Settings / Config / CSS VARS
// ==========================================================================
:root {
// Grid
--grid-columns: 4;
--grid-gutter: #{rem(10px)};
--grid-margin: #{rem(10px)};
// Container
--container-width: calc(100% - 2 * var(--grid-margin));
@media (min-width: $from-sm) {
--grid-columns: #{$base-column-nb};
--grid-gutter: #{rem(16px)};
--grid-margin: #{rem(20px)};
}
}

View File

@@ -1,44 +0,0 @@
// ==========================================================================
// Settings / Config / Z-indexes
// ==========================================================================
// Timings
// ==========================================================================
$z-indexes: (
"header": 200,
"above": 1,
"default": 0,
"below": -1
);
// Default z-index for z()
$z-index-default: "above" !default;
// Function
// ==========================================================================
// Retrieves the z-index from the {@see $layers master list}.
//
// @link on http://css-tricks.com/handling-z-index/
//
// @param {string} $layer The name of the z-index.
// @param {number} $modifier A positive or negative modifier to apply
// to the returned z-index value.
// @throw Error if the $layer does not exist.
// @throw Warning if the $modifier might overlap another master z-index.
// @return {number} The computed z-index of $layer and $modifier.
@function z($layer: $z-index-default, $modifier: 0) {
@if not map-has-key($z-indexes, $layer) {
@error "Unknown master z-index layer: #{$layer}";
}
@if ($modifier >= 50 or $modifier <= -50) {
@warn "Modifier may overlap the another master z-index layer: #{$modifier}";
}
$index: map-get($z-indexes, $layer);
@return $index + $modifier;
}

View File

@@ -1,14 +1,12 @@
// ==========================================================================
// Tools / Family
// ==========================================================================
//
//
// DOCS : https://lukyvj.github.io/family.scss/
//
// Select all children from the first to `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
//
/// Select all children from the first to `$num`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin first($num) {
@if $num == 1 {
&:first-child {
@@ -21,118 +19,108 @@
}
}
// Select all children from the last to `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select all children from the last to `$num`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin last($num) {
&:nth-last-child(-n + #{$num}) {
@content;
}
}
// Select all children after the first to `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select all children after the first to `$num`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin after-first($num) {
&:nth-child(n + #{$num + 1}) {
@content;
}
}
// Select all children before `$num` from the last.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select all children before `$num` from the last.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin from-end($num) {
&:nth-last-child(#{$num}) {
@content;
}
}
// Select all children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select all children between `$first` and `$last`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin between($first, $last) {
&:nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all even children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select all even children between `$first` and `$last`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin even-between($first, $last) {
&:nth-child(even):nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all odd children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select all odd children between `$first` and `$last`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin odd-between($first, $last) {
&:nth-child(odd):nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all `$num` children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select all `$num` children between `$first` and `$last`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin n-between($num, $first, $last) {
&:nth-child(#{$num}n):nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all children but `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select all children but `$num`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin all-but($num) {
&:not(:nth-child(#{$num})) {
@content;
}
}
// Select children each `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
// @alias every
/// Select children each `$num`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
/// @alias every
@mixin each($num) {
&:nth-child(#{$num}n) {
@content;
}
}
// Select children each `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select children each `$num`.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin every($num) {
&:nth-child(#{$num}n) {
@content;
}
}
// Select the `$num` child from the start and the `$num` child from the last.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select the `$num` child from the start and the `$num` child from the last.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin from-first-last($num) {
&:nth-child(#{$num}),
&:nth-last-child(#{$num}) {
@@ -140,59 +128,57 @@
}
}
// Select the item in the middle of `$num` child. Only works with odd number
// chain.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select the item in the middle of `$num` child. Only works with odd number
/// chain.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin middle($num) {
&:nth-child(#{round(math.div($num, 2))}) {
&:nth-child(#{round($num / 2)}) {
@content;
}
}
// Select all children between the `$num` first and the `$num` last.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
/// Select all children between the `$num` first and the `$num` last.
/// @group with-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - id of the child
@mixin all-but-first-last($num) {
&:nth-child(n + #{$num}):nth-last-child(n + #{$num}) {
@content;
}
}
// This quantity-query mixin will only select the first of `$limit` items. It will not
// work if there is not as much as item as you set in `$limit`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// This quantity-query mixin will only select the first of `$limit` items. It will not
/// work if there is not as much as item as you set in `$limit`.
/// @group Quantity queries
/// @param {number} $limit
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin first-of($limit) {
&:nth-last-child(#{$limit}):first-child {
@content;
}
}
// This quantity-query mixin will only select the last of `$limit` items. It will not
// if there is not as much as item as you set in `$limit`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// This quantity-query mixin will only select the last of `$limit` items. It will not
/// if there is not as much as item as you set in `$limit`.
/// @group Quantity queries
/// @param {number} $limit
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin last-of($limit) {
&:nth-of-type(#{$limit}):nth-last-of-type(1) {
@content;
}
}
// This quantity-query mixin will select every items if there is at least `$num` items. It will not
// if there is not as much as item as you set in `$num`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// This quantity-query mixin will select every items if there is at least `$num` items. It will not
/// if there is not as much as item as you set in `$num`.
/// @group Quantity queries
/// @param {number} $limit
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin at-least($num) {
$selector: &;
$child: nth(nth($selector, -1), -1);
@@ -203,12 +189,11 @@
}
}
// This quantity-query mixin will select every items if there is at most `$num` items. It will not
// if there is not as much as item as you set in `$num`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// This quantity-query mixin will select every items if there is at most `$num` items. It will not
/// if there is not as much as item as you set in `$num`.
/// @group Quantity queries
/// @param {number} $limit
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin at-most($num) {
$selector: &;
$child: nth(nth($selector, -1), -1);
@@ -219,11 +204,10 @@
}
}
// This quantity-query mixin will select every items only if there is between `$min` and `$max` items.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// This quantity-query mixin will select every items only if there is between `$min` and `$max` items.
/// @group Quantity queries
/// @param {number} $limit
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin in-between($min, $max) {
$selector: &;
$child: nth(nth($selector, -1), -1);
@@ -234,50 +218,45 @@
}
}
// Select the first exact child
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select the first exact child
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin first-child() {
&:first-of-type {
@content
}
}
// Select the last exact child
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select the last exact child
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin last-child() {
&:last-of-type {
@content
}
}
// Select all even children.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select all even children.
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin even() {
&:nth-child(even) {
@content;
}
}
// Select all odd children.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select all odd children.
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin odd() {
&:nth-child(odd) {
@content;
}
}
// Select only the first and last child.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Select only the first and last child.
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin first-last() {
&:first-child,
&:last-child {
@@ -285,46 +264,43 @@
}
}
// Will only select the child if its unique.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @alias only
/// Will only select the child if its unique.
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @alias only
@mixin unique() {
&:only-child {
@content;
}
}
// Will only select the child if its unique.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Will only select the child if its unique.
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin only() {
&:only-child {
@content;
}
}
// Will only select children if they are not unique. Meaning if there is at
// least 2 children, the style is applied.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// Will only select children if they are not unique. Meaning if there is at
/// least 2 children, the style is applied.
/// @group no-arguments
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin not-unique() {
&:not(:only-child) {
@content;
}
}
// This mixin is used to automatically sort z-index in numerical order. But it
// can also sort them in anti-numerical order, depending the parameters you use.
// @group using functions
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - Number of children
// @param {string} $direction [forward] - Direction of the sort
// @param {number} $index [0] - Index of the sorting
/// This mixin is used to automatically sort z-index in numerical order. But it
/// can also sort them in anti-numerical order, depending the parameters you use.
/// @group using functions
/// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
/// @param {number} $num - Number of children
/// @param {string} $direction [forward] - Direction of the sort
/// @param {number} $index [0] - Index of the sorting
@mixin child-index($num, $direction: 'forward', $index: 0) {
@for $i from 1 through $num {
@if ($direction == 'forward') {
@@ -341,12 +317,11 @@
}
}
// Used by the child-index mixin. It will returned the proper sorted numbers
// depending on the `$index` value.
// @access private
// @param {number} $num - Number of children
// @param {number} $index - Index of the sorting
/// Used by the child-index mixin. It will returned the proper sorted numbers
/// depending on the `$index` value.
/// @access private
/// @param {number} $num - Number of children
/// @param {number} $index - Index of the sorting
@function order-index($i, $index) {
@return ($index + $i);
}

View File

@@ -0,0 +1,98 @@
// ==========================================================================
// Tools / Font Faces
// ==========================================================================
$global-font-file-formats: "woff2", "woff" !default;
//
// Builds the `src` list for an `@font-face` declaration.
//
// @link https://github.com/thoughtbot/bourbon/blob/master/core/bourbon/utilities/_font-source-declaration.scss
// @link http://goo.gl/Ru1bKP
//
// @param {String} $font-family - The font family name.
// @param {String} $file-path - The path to the font family.
// @param {List} $file-formats - The file formats to request.
// @return {List}
//
// @require {function} list-contains
//
// @access private
//
@function font-source-declaration(
$font-family,
$file-path,
$file-formats
) {
$src: ();
$formats-map: (
eot: "#{$file-path}.eot?#iefix" format("embedded-opentype"),
woff2: "#{$file-path}.woff2" format("woff2"),
woff: "#{$file-path}.woff" format("woff"),
ttf: "#{$file-path}.ttf" format("truetype"),
svg: "#{$file-path}.svg##{$font-family}" format("svg"),
);
@each $key, $values in $formats-map {
@if list-contains($file-formats, $key) {
$file-path: nth($values, 1);
$font-format: nth($values, 2);
$src: append($src, url("#{$assets-path}#{$file-path}") $font-format, comma);
}
}
@return $src;
}
//
// Generates an `@font-face` declaration.
//
// You can choose the specific file formats you need to output; the mixin supports
// `eot`, `ttf`, `svg`, `woff2` and `woff`.
//
// @link https://github.com/thoughtbot/bourbon/blob/master/core/bourbon/library/_font-face.scss
//
// @param {String} $font-family - The font family name.
// @param {String} $file-path - The path to the font family.
// @param {String|List} $file-formats [("ttf", "woff2", "woff")]
// A list of file formats to support,
// for example ("eot", "ttf", "svg", "woff2", "woff").
//
// @content
// Any additional CSS properties that are included in the `@include`
// directive will be output within the `@font-face` declaration, e.g.
// you can pass in `font-weight`, `font-style` and/or `unicode-range`.
//
// @example scss
// @include font-face(
// "source-sans-pro",
// "fonts/source-sans-pro-regular",
// ("woff2", "woff")
// ) {
// font-style: normal;
// font-weight: 400;
// }
//
// // CSS Output
// @font-face {
// font-family: "source-sans-pro";
// src: url("fonts/source-sans-pro-regular.woff2") format("woff2"),
// url("fonts/source-sans-pro-regular.woff") format("woff");
// font-style: normal;
// font-weight: 400;
// }
//
// @require {function} _font-source-declaration
// @require {function} _retrieve-bourbon-setting
//
@mixin font-face(
$font-family,
$file-path,
$file-formats: $global-font-file-formats
) {
@font-face {
font-family: $font-family;
src: font-source-declaration( $font-family, $file-path, $file-formats);
@content;
}
}

View File

@@ -1,53 +1,73 @@
// ==========================================================================
// Tools / Functions
// ==========================================================================
// Check if the given value is a number in pixel
//
// @param {Number} $number - The value to check
// @return {Boolean}
@function is-pixel-number($number) {
@return type-of($number) == number and unit($number) == "px";
}
// Converts the given pixel value to its EM quivalent.
//
// @param {Number} $size - The pixel value to convert.
// @param {Number} $base [$font-size] - The assumed base font size.
// @return {Number} Scalable pixel value in EMs.
//
@function em($size, $base: $font-size) {
@if not is-pixel-number($size) {
@error "`#{$size}` needs to be a number in pixel.";
@if (type-of($size) == number) {
@if (unit($size) != "px") {
@error "`#{$size}` needs to be a pixel value.";
}
} @else {
@error "`#{$size}` needs to be a number.";
}
@if not is-pixel-number($base) {
@error "`#{$base}` needs to be a number in pixel.";
@if (type-of($base) == number) {
@if (unit($base) != "px") {
@error "`#{$base}` needs to be a pixel value.";
}
} @else {
@error "`#{$base}` needs to be a number.";
}
@return math.div($size, $base) * 1em;
@return ($size / $base) * 1em;
}
//
// Converts the given pixel value to its REM quivalent.
//
// @param {Number} $size - The pixel value to convert.
// @param {Number} $base [$font-size] - The assumed base font size.
// @return {Number} Scalable pixel value in REMs.
//
@function rem($size, $base: $font-size) {
@if not is-pixel-number($size) {
@error "`#{$size}` needs to be a number in pixel.";
@if (type-of($size) == number) {
@if (unit($size) != "px") {
@error "`#{$size}` needs to be a pixel value.";
}
} @else {
@error "`#{$size}` needs to be a number.";
}
@if not is-pixel-number($base) {
@error "`#{$base}` needs to be a number in pixel.";
@if (type-of($base) == number) {
@if (unit($base) != "px") {
@error "`#{$base}` needs to be a pixel value.";
}
} @else {
@error "`#{$base}` needs to be a number.";
}
@return math.div($size, $base) * 1rem;
@return ($size / $base) * 1rem;
}
//
// Converts a number to a percentage.
//
// @alias percentage()
// @link http://sassdoc.com/annotations/#alias
// @param {Number} $number - The value to convert.
// @return {Number} A percentage.
//
@function span($number) {
@return percentage($number);
}
//
// Checks if a list contains a value(s).
//
// @link https://github.com/thoughtbot/bourbon/blob/master/core/bourbon/validators/_contains.scss
@@ -55,7 +75,7 @@
// @param {List} $values - A single value or list of values to check for.
// @return {Boolean}
// @access private
//
@function list-contains(
$list,
$values...
@@ -69,146 +89,40 @@
@return true;
}
//
// Resolve whether a rule is important or not.
//
// @param {Boolean} $flag - Whether a rule is important (TRUE) or not (FALSE).
// @return {String|Null} Returns `!important` or NULL.
//
@function important($flag: false) {
@if ($flag == true) {
@return !important;
} @else if ($important == false) {
} @elseif ($important == false) {
@return null;
} @else {
@error "`#{$flag}` needs to be `true` or `false`.";
@error "`#{$flag}` needs to be `true` or `false`."
}
}
//
// Determine if the current context is for a WYSIWYG editor.
//
// @requires {String} $context - The global context of the stylesheet.
// @return {Boolean} If the $context is set to "editor".
//
@function is-editor() {
@return ('editor' == $context);
}
//
// Determine if the current context is for the front-end.
//
// @requires {String} $context - The global context of the stylesheet.
// @return {Boolean} If the $context is set to "frontend".
//
@function is-frontend() {
@return ('frontend' == $context);
}
$context: 'frontend' !default;
// Returns calculation of a percentage of the grid cell width
// with optional inset of grid gutter.
//
// ```scss
// .c-box {
// width: grid-space(6/12);
// margin-left: grid-space(1/12, 1);
// }
// ```
//
// @param {number} $percentage - The percentage spacer
// @param {number} $inset - The grid gutter inset
// @return {function<number>}
@function grid-space($percentage, $inset: 0) {
@return calc(#{$percentage} * (#{vw(100)} - 2 * var(--grid-margin, 0px)) - (1 - #{$percentage}) * var(--grid-gutter, 0px) + #{$inset} * var(--grid-gutter, 0px));
}
// Returns calculation of a percentage of the viewport small height.
//
// ```scss
// .c-box {
// height: svh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function<number>} in svh
@function svh($number) {
@return calc(#{$number} * var(--svh, 1svh));
}
// Returns calculation of a percentage of the viewport large height.
//
// ```scss
// .c-box {
// height: lvh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function<number>} in lvh
@function lvh($number) {
@return calc(#{$number} * var(--lvh, 1lvh));
}
// Returns calculation of a percentage of the viewport dynamic height.
//
// ```scss
// .c-box {
// height: dvh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function<number>} in dvh
@function dvh($number) {
@return calc(#{$number} * var(--dvh, 1dvh));
}
// Returns calculation of a percentage of the viewport width.
//
// ```scss
// .c-box {
// width: vw(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function<number>} in vw
@function vw($number) {
@return calc(#{$number} * var(--vw, 1vw));
}
@function clamp-with-max($min, $size, $max) {
$vw-context: $vw-viewport * 0.01;
@return clamp(#{$min}, calc(#{$size} / #{$vw-context} * 1vw), #{$max});
}
@function size-clamp($size) {
@return clamp-with-max(
calc(#{rem(1px)} * var(--spacing-#{$size}-mobile)),
var(--spacing-#{$size}-desktop),
calc(#{rem(1px)} * var(--spacing-#{$size}-desktop))
);
}
// Returns clamp of calculated preferred responsive font size
// within a font size and breakpoint range.
//
// ```scss
// .c-heading.-h1 {
// font-size: responsive-value(30px, 60px, 1800);
// }
//
// .c-heading.-h2 {
// font-size: responsive-value(20px, 40px, $from-xl);
// }
// ```
//
// @param {number} $min-size - Minimum font size in pixels.
// @param {number} $max-size - Maximum font size in pixels.
// @param {number} $breakpoint - Maximum breakpoint.
// @return {function<number, function<number>, number>}
@function responsive-value($min-size, $max-size, $breakpoint) {
$delta: math.div($max-size, $breakpoint);
@return clamp($min-size, calc(#{strip-unit($delta)} * #{vw(100)}), $max-size);
}

View File

@@ -2,6 +2,7 @@
// Tools / Layout
// ==========================================================================
//
// Grid-like layout system.
//
// The layout tools provide a column-style layout system. This file contains
@@ -9,6 +10,7 @@
//
// @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss
//
//
// Generate the layout container.
//
@@ -17,7 +19,7 @@
//
// @requires {function} u-list-reset
// @output `font-size`, `margin`, `padding`, `list-style`
//
@mixin o-layout($gutter: 0, $fix-whitespace: true) {
margin: 0;
padding: 0;
@@ -32,6 +34,7 @@
}
}
//
// Generate the layout item.
//
// 1. Required in order to combine fluid widths with fixed gutters.
@@ -42,7 +45,7 @@
// 4. By default, all layout items are full-width (mobile first).
// 5. Gutters provided by left padding:
// http://csswizardry.com/2011/08/building-better-grid-systems/
//
@mixin o-layout_item($gutter: 0, $fix-whitespace: true) {
display: inline-block; // [2]
width: 100%; // [4]

View File

@@ -1,141 +0,0 @@
// ==========================================================================
// Tools / Maths
// ==========================================================================
// Remove the unit of a length
//
// @param {Number} $number Number to remove unit from
// @return {function<number>}
@function strip-unit($value) {
@if type-of($value) != "number" {
@error "Invalid `#{type-of($value)}` type. Choose a number type instead.";
} @else if type-of($value) == "number" and not is-unitless($value) {
@return math.div($value, $value * 0 + 1);
}
@return $value;
}
// Returns the square root of the given number.
//
// @param {number} $number The number to calculate.
// @return {number}
@function sqrt($number) {
$x: 1;
$value: $x;
@for $i from 1 through 10 {
$value: $x - math.div(($x * $x - abs($number)), (2 * $x));
$x: $value;
}
@return $value;
}
// Returns a number raised to the power of an exponent.
//
// @param {number} $number The base number.
// @param {number} $exp The exponent.
// @return {number}
@function pow($number, $exp) {
$value: 1;
@if $exp > 0 {
@for $i from 1 through $exp {
$value: $value * $number;
}
} @else if $exp < 0 {
@for $i from 1 through -$exp {
$value: math.div($value, $number);
}
}
@return $value;
}
// Returns the factorial of the given number.
//
// @param {number} $number The number to calculate.
// @return {number}
@function fact($number) {
$value: 1;
@if $number > 0 {
@for $i from 1 through $number {
$value: $value * $i;
}
}
@return $value;
}
// Returns an approximation of pi, with 11 decimals.
//
// @return {number}
@function pi() {
@return 3.14159265359;
}
// Converts the number in degrees to the radian equivalent .
//
// @param {number} $angle The angular value to calculate.
// @return {number} If $angle has the `deg` unit,
// the radian equivalent is returned.
// Otherwise, the unitless value of $angle is returned.
@function rad($angle) {
$unit: unit($angle);
$angle: strip-units($angle);
// If the angle has `deg` as unit, convert to radians.
@if ($unit == deg) {
@return math.div($angle, 180) * pi();
}
@return $angle;
}
// Returns the sine of the given number.
//
// @param {number} $angle The angle to calculate.
// @return {number}
@function sin($angle) {
$sin: 0;
$angle: rad($angle);
@for $i from 0 through 10 {
$sin: $sin + pow(-1, $i) * math.div(pow($angle, (2 * $i + 1)), fact(2 * $i + 1));
}
@return $sin;
}
// Returns the cosine of the given number.
//
// @param {string} $angle The angle to calculate.
// @return {number}
@function cos($angle) {
$cos: 0;
$angle: rad($angle);
@for $i from 0 through 10 {
$cos: $cos + pow(-1, $i) * math.div(pow($angle, 2 * $i), fact(2 * $i));
}
@return $cos;
}
// Returns the tangent of the given number.
//
// @param {string} $angle The angle to calculate.
// @return {number}
@function tan($angle) {
@return math.div(sin($angle), cos($angle));
}

View File

@@ -2,34 +2,37 @@
// Tools / Mixins
// ==========================================================================
//
// Set the color of the highlight that appears over a link while it's being tapped.
//
// By default, the highlight is suppressed.
//
// @param {Color} $value [rgba(0, 0, 0, 0)] - The value of the highlight.
// @output `-webkit-tap-highlight-color`
//
@mixin tap-highlight-color($value: rgba(0, 0, 0, 0)) {
-webkit-tap-highlight-color: $value;
}
//
// Set whether or not touch devices use momentum-based scrolling for the given element.
//
// By default, applies momentum-based scrolling for the current element.
//
// @param {String} $value [rgba(0, 0, 0, 0)] - The type of scrolling.
// @output `-webkit-overflow-scrolling`
//
@mixin overflow-scrolling($value: touch) {
-webkit-overflow-scrolling: $value;
}
//
// Micro clearfix rules for containing floats.
//
// @link http://www.cssmojo.com/the-very-latest-clearfix-reloaded/
// @param {List} $supports The type of clearfix to generate.
// @output Injects `:::after` pseudo-element.
//
@mixin u-clearfix($supports...) {
&::after {
display: if(list-contains($supports, table), table, block);
@@ -38,6 +41,7 @@
}
}
//
// Generate a font-size and baseline-compatible line-height.
//
// @link https://github.com/inuitcss/inuitcss/c14029c/tools/_tools.font-size.scss
@@ -45,24 +49,25 @@
// @param {Number} $line-height [auto] - The line box height.
// @param {Boolean} $important [false] - Whether the font-size is important.
// @output `font-size`, `line-height`
//
@mixin font-size($font-size, $line-height: auto, $important: false) {
$important: important($important);
font-size: rem($font-size) $important;
@if ($line-height == "auto") {
line-height: ceil(math.div($font-size, $line-height)) * math.div($line-height, $font-size) $important;
line-height: ceil($font-size / $line-height) * ($line-height / $font-size) $important;
}
@else {
@if (type-of($line-height) == number or $line-height == "inherit" or $line-height == "normal") {
line-height: $line-height $important;
}
@else if ($line-height != "none" and $line-height != false) {
@error "Doh! `#{$line-height}` is not a valid value for `$line-height`.";
@elseif ($line-height != "none" and $line-height != false) {
@error "Doh! `#{$line-height}` is not a valid value for `$line-height`."
}
}
}
//
// Vertically-center the direct descendants of the current element.
//
// Centering is achieved by displaying children as inline-blocks. Any whitespace
@@ -70,7 +75,7 @@
// and its children.
//
// @output `font-size`, `display`, `vertical-align`
//
@mixin o-vertical-center {
font-size: 0;
@@ -88,12 +93,13 @@
}
}
//
// Generate `:hover` and `:focus` styles in one go.
//
// @link https://github.com/inuitcss/inuitcss/blob/master/tools/_tools.mixins.scss
// @content Wrapped in `:focus` and `:hover` pseudo-classes.
// @output Wraps the given content in `:focus` and `:hover` pseudo-classes.
//
@mixin u-hocus {
&:focus,
&:hover {
@@ -101,12 +107,13 @@
}
}
//
// Generate `:active` and `:focus` styles in one go.
//
// @see {Mixin} u-hocus
// @content Wrapped in `:focus` and `:active` pseudo-classes.
// @output Wraps the given content in `:focus` and `:hover` pseudo-classes.
//
@mixin u-actus {
&:focus,
&:active {
@@ -114,6 +121,7 @@
}
}
//
// Prevent text from wrapping onto multiple lines for the current element.
//
// An ellipsis is appended to the end of the line.
@@ -123,7 +131,7 @@
//
// @param {Number} $width [100%] - The maximum width of element.
// @output `max-width`, `word-wrap`, `white-space`, `overflow`, `text-overflow`
//
@mixin u-truncate($width: 100%) {
overflow: hidden;
text-overflow: ellipsis;
@@ -134,11 +142,12 @@
}
}
//
// Applies accessible hiding to the current element.
//
// @param {Boolean} $important [true] - Whether the visibility is important.
// @output Properties for removing the element from the document flow.
//
@mixin u-accessibly-hidden($important: true) {
$important: important($important);
position: absolute $important;
@@ -151,11 +160,12 @@
border: 0;
}
//
// Allows an accessibly hidden element to be focusable via keyboard navigation.
//
// @content For styling the now visible element.
// @output Injects `:focus`, `:active` pseudo-classes.
//
@mixin u-accessibly-focusable {
@include u-actus {
clip: auto;
@@ -166,6 +176,7 @@
}
}
//
// Hide the current element from all.
//
// The element will be hidden from screen readers and removed from the document flow.
@@ -173,13 +184,14 @@
// @link http://juicystudio.com/article/screen-readers-display-none.php
// @param {Boolean} $important [true] - Whether the visibility is important.
// @output `display`, `visibility`
//
@mixin u-hidden($important: true) {
$important: important($important);
display: none $important;
visibility: hidden $important;
}
//
// Show the current element for all.
//
// The element will be accessible from screen readers and visible in the document flow.
@@ -187,38 +199,9 @@
// @param {String} $display [block] - The rendering box used for the element.
// @param {Boolean} $important [true] - Whether the visibility is important.
// @output `display`, `visibility`
//
@mixin u-shown($display: block, $important: true) {
$important: important($important);
display: $display $important;
visibility: visible $important;
}
// Aspect-ratio polyfill
//
// @param {Number} $ratio [19/6] - The ratio of the element.
// @param {Number} $width [100%] - The fallback width of element.
// @param {Boolean} $children [false] - Whether the element contains children for the fallback properties.
// @output Properties for maintaining aspect-ratio
@mixin aspect-ratio($ratio: math.div(16, 9), $width: 100%, $children: false) {
@supports (aspect-ratio: 1) {
aspect-ratio: $ratio;
}
@supports not (aspect-ratio: 1) {
height: 0;
padding-top: calc(#{$width} * #{math.div(1, $ratio)});
@if ($children == true) {
position: relative;
> * {
position: absolute;
top: 0;
left: 0;
}
}
}
}

View File

@@ -10,21 +10,19 @@
// .u-pull-2/4
// .u-pull-1/5
// .u-push-2/3
$widths-offsets: false !default;
// By default, the boilerplate uses fractions-like classes like `<div class="u-1/4">`.
// You can change the `/` to whatever you fancy with this variable.
$fractions-delimiter: \/ !default;
// When using Sass-MQ, this defines the separator for the breakpoints suffix
// in the class name. By default, we are generating the responsive suffixes
// for the classes with a `@` symbol so you get classes like:
// <div class="u-3/12@mobile">
$breakpoint-delimiter: \@ !default;
//
// Generate a series of width helper classes
//
// @example scss
@@ -47,7 +45,7 @@ $breakpoint-delimiter: \@ !default;
// @param {List} $colums - The columns we want the widths to have.
// @param {String} $breakpoint - Optional suffix for responsive widths.
// @output `width`, `position`, `right`, `left`
//
@mixin widths($columns, $breakpoint: null, $important: true) {
$important: important($important);
@@ -58,7 +56,7 @@ $breakpoint-delimiter: \@ !default;
@for $numerator from 1 through $denominator {
// Build a class in the format `.u-3/4[@<breakpoint>]`.
.u-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
width: math.div($numerator, $denominator) * 100% $important;
width: ($numerator / $denominator) * 100% $important;
}
@if ($widths-offsets == true) {
@@ -66,13 +64,13 @@ $breakpoint-delimiter: \@ !default;
.u-push-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
position: relative $important;
right: auto $important;
left: math.div($numerator, $denominator) * 100% $important;
left: ($numerator / $denominator) * 100% $important;
}
// Build a class in the format `.u-pull-5/6[@<breakpoint>]`.
.u-pull-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
position: relative $important;
right: math.div($numerator, $denominator) * 100% $important;
right: ($numerator / $denominator) * 100% $important;
left: auto $important;
}
}

View File

@@ -0,0 +1,38 @@
// ==========================================================================
// Typography / Headings
// ==========================================================================
.t-h1,
.t-h2,
.t-h3,
.t-h4,
.t-h5,
.t-h6 {
--heading-line-height: #{$line-height};
line-height: var(--heading-line-height);
}
.t-h1 {
font-size: var(--font-size-h1);
}
.t-h2 {
font-size: var(--font-size-h2);
}
.t-h3 {
font-size: var(--font-size-h3);
}
.t-h4 {
font-size: var(--font-size-h4);
}
.t-h5 {
font-size: var(--font-size-h5);
}
.t-h6 {
font-size: var(--font-size-h6);
}

View File

@@ -0,0 +1,55 @@
// ==========================================================================
// Typography / Wysiwyg
// ==========================================================================
.t-wysiwyg {
p,
ul,
ol,
blockquote {
margin-bottom: $line-height * 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1em 0 0.5em;
}
h1 { @extend .t-h1; }
h2 { @extend .t-h2; }
h3 { @extend .t-h3; }
h4 { @extend .t-h4; }
h5 { @extend .t-h5; }
h6 { @extend .t-h6; }
ul,
ol {
padding-left: 2em;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
b,
strong {
font-weight: 700;
}
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
}

View File

@@ -4,7 +4,6 @@
// Floats
// ==========================================================================
.u-float-left {
float: left !important;
}
@@ -15,7 +14,6 @@
// Horizontal Text
// ==========================================================================
.u-text-center {
text-align: center !important;
}
@@ -30,7 +28,6 @@
// Vertical Text
// ==========================================================================
.u-align-baseline {
vertical-align: baseline !important;
}

View File

@@ -1,43 +0,0 @@
// ==========================================================================
// Tools / Grid Columns
// ==========================================================================
//
// Grid layout system.
//
// This tool generates columns for all needed media queries.
// Unused classes will be purge by the css post-processor.
//
$colsMax: $base-column-nb + 1;
@each $breakpoint, $mediaquery in $breakpoints {
@for $fromIndex from 1 through $colsMax {
@for $toIndex from 1 through $colsMax {
// Columns without media query
@if $breakpoint == "tiny" {
.u-gc-#{$fromIndex}\/#{$toIndex} {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
// Columns min-width breakpoints `@from-*`
.u-gc-#{$fromIndex}\/#{$toIndex}\@from-#{$breakpoint} {
@media #{mq-min($breakpoint)} {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
// Columns max-width breakpoints @to-*`
.u-gc-#{$fromIndex}\/#{$toIndex}\@to-#{$breakpoint} {
@media #{mq-max($breakpoint)} {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
}
}
}

View File

@@ -4,21 +4,18 @@
// Layout
// ==========================================================================
.u-clearfix {
@include u-clearfix;
}
// Decorative
// =============================================================================
.u-truncate {
@include u-truncate;
}
// Visibility / Display
// ==========================================================================
[hidden][aria-hidden="false"] {
position: absolute;
display: inherit;
@@ -33,33 +30,35 @@
// display: block;
// }
// // 1. Fix for Firefox bug: an image styled `max-width:100%` within an
// // inline-block will display at its default size, and not limit its width to
// // 100% of an ancestral container.
//
// /**
// * 1. Fix for Firefox bug: an image styled `max-width:100%` within an
// * inline-block will display at its default size, and not limit its width to
// * 100% of an ancestral container.
// */
// .u-inline-block {
// display: inline-block !important;
// max-width: 100%; /* 1 */
// }
//
// .u-inline {
// display: inline !important;
// }
//
// .u-table {
// display: table !important;
// }
//
// .u-tableCell {
// display: table-cell !important;
// }
//
// .u-tableRow {
// display: table-row !important;
// }
// Completely remove from the flow but leave available to screen readers.
/**
* Completely remove from the flow but leave available to screen readers.
*/
.u-screen-reader-text {
@include u-accessibly-hidden;
}
@@ -70,12 +69,13 @@
}
}
// Extends the `.screen-reader-text` class to allow the element
// to be focusable when navigated to via the keyboard.
//
// @link https://www.drupal.org/node/897638
// @todo Define styles when focused.
/*
* Extends the `.screen-reader-text` class to allow the element
* to be focusable when navigated to via the keyboard.
*
* @link https://www.drupal.org/node/897638
* @todo Define styles when focused.
*/
.u-screen-reader-text.-focusable {
@include u-accessibly-focusable;
}

View File

@@ -11,9 +11,9 @@
////
@media print {
// 1. Black prints faster: http://www.sanbeiji.com/archives/953
/**
* 1. Black prints faster: http://www.sanbeiji.com/archives/953
*/
*,
*:before,
*:after,
@@ -21,7 +21,7 @@
*:first-line {
background: transparent !important;
box-shadow: none !important;
color: #000000 !important; // [1]
color: #000000 !important; /* [1] */
text-shadow: none !important;
}
@@ -38,9 +38,10 @@
content: " (" attr(title) ")";
}
// Don't show links that are fragment identifiers, or use the `javascript:`
// pseudo protocol.
/**
* Don't show links that are fragment identifiers, or use the `javascript:`
* pseudo protocol.
*/
a[href^="#"]:after,
a[href^="javascript:"]:after {
content: "";
@@ -52,8 +53,9 @@
page-break-inside: avoid;
}
// Printing Tables: http://css-discuss.incutio.com/wiki/Printing_Tables
/**
* Printing Tables: http://css-discuss.incutio.com/wiki/Printing_Tables
*/
thead {
display: table-header-group;
}

View File

@@ -2,9 +2,12 @@
// Utilities / Ratio
// ==========================================================================
//
// @link https://github.com/inuitcss/inuitcss/blob/19d0c7e/objects/_objects.ratio.scss
//
// A list of aspect ratios that get generated as modifier classes.
$aspect-ratios: (
(2:1),
(4:3),
@@ -13,11 +16,13 @@ $aspect-ratios: (
/* stylelint-disable */
//
// Generate a series of ratio classes to be used like so:
//
// @example
// <div class="o-ratio u-16:9">
//
//
@each $ratio in $aspect-ratios {
@each $antecedent, $consequent in $ratio {
@if (type-of($antecedent) != number) {
@@ -28,8 +33,8 @@ $aspect-ratios: (
@error "`#{$consequent}` needs to be a number."
}
.u-#{$antecedent}\:#{$consequent}::before {
padding-bottom: math.div($consequent, $antecedent) * 100%;
&.u-#{$antecedent}\:#{$consequent}::before {
padding-bottom: ($consequent/$antecedent) * 100%;
}
}
}

View File

@@ -8,11 +8,12 @@
///
/// @example
/// .u-margin-top {}
/// .u-margin-top-xs {}
/// .u-padding-left-lg {}
/// .u-margin-right-sm {}
/// .u-padding-left-large {}
/// .u-margin-right-small {}
/// .u-padding {}
/// .u-padding-right-none {}
/// .u-padding-horizontal {}
/// .u-padding-vertical-small {}
///
/// @link https://github.com/inuitcss/inuitcss/blob/512977a/utilities/_utilities.spacing.scss
////
@@ -25,8 +26,8 @@ $spacing-directions: (
'-right': '-right',
'-bottom': '-bottom',
'-left': '-left',
'-x': '-left' '-right',
'-y': '-top' '-bottom',
'-horizontal': '-left' '-right',
'-vertical': '-top' '-bottom',
) !default;
$spacing-properties: (
@@ -34,47 +35,19 @@ $spacing-properties: (
'margin': 'margin',
) !default;
$spacing-sizes: join($spacings, (
null: var(--grid-gutter),
'none': 0
));
$spacing-sizes: (
null: $unit,
'-double': $unit * 2,
'-small': $unit-small,
'-none': 0px
) !default;
@each $breakpoint, $mediaquery in $breakpoints {
@each $property-namespace, $property in $spacing-properties {
@each $direction-namespace, $directions in $spacing-directions {
@each $size-namespace, $size in $spacing-sizes {
// Prepend "-" to spacing sizes if not null
$size-namespace: if($size-namespace != null, "-" + $size-namespace, $size-namespace);
// Base class
$base-class: ".u-" + #{$property-namespace}#{$direction-namespace}#{$size-namespace};
// Spacing without media query
@if $breakpoint == "xs" {
#{$base-class} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
// Spacing min-width breakpoints `@from-*`
#{$base-class}\@from-#{$breakpoint} {
@media #{mq-min($breakpoint)} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
// Spacing max-width breakpoints @to-*`
#{$base-class}\@to-#{$breakpoint} {
@media #{mq-max($breakpoint)} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
@each $property-namespace, $property in $spacing-properties {
@each $direction-namespace, $direction-rules in $spacing-directions {
@each $size-namespace, $size in $spacing-sizes {
.u-#{$property-namespace}#{$direction-namespace}#{$size-namespace} {
@each $direction in $direction-rules {
#{$property}#{$direction}: rem($size) !important;
}
}
}

View File

@@ -2,8 +2,9 @@
// Utilities / States
// ==========================================================================
// ARIA roles display visual cursor hints
/**
* ARIA roles display visual cursor hints
*/
[aria-busy="true"] {
cursor: progress;
}
@@ -16,7 +17,9 @@
cursor: default;
}
// Control visibility without affecting flow.
/**
* Control visibility without affecting flow.
*/
.is-visible {
visibility: visible !important;
@@ -28,7 +31,9 @@
opacity: 0 !important;
}
// Completely remove from the flow and screen readers.
/**
* Completely remove from the flow and screen readers.
*/
.is-hidden {
@include u-hidden;
@@ -46,32 +51,34 @@
}
}
// .is-hidden\@to-lg {
// @media (max-width: $to-lg) {
// display: none;
// }
// }
//
// .is-hidden\@from-lg {
// @media (min-width: $from-lg) {
// .is-hidden\@to-large {
// @media (max-width: $to-large) {
// display: none;
// }
// }
// // Display a hidden-by-default element.
//
// .is-hidden\@from-large {
// @media (min-width: $from-large) {
// display: none;
// }
// }
// /**
// * Display a hidden-by-default element.
// */
// .is-shown {
// @include u-shown;
// }
//
// table.is-shown {
// display: table !important;
// }
//
// tr.is-shown {
// display: table-row !important;
// }
//
// td.is-shown,
// th.is-shown {
// display: table-cell !important;

View File

@@ -4,8 +4,8 @@
////
/// @link https://github.com/inuitcss/inuitcss/blob/6eb574f/utilities/_utilities.widths.scss
///
///
////
/// Which fractions would you like in your grid system(s)?
/// By default, the boilerplate provides fractions of one whole, halves, thirds,
/// quarters, and fifths, e.g.:
@@ -15,14 +15,12 @@
/// .u-2/5
/// .u-3/4
/// .u-2/3
////
$widths-fractions: 1 2 3 4 5 !default;
@include widths($widths-fractions);
.u-1\/2\@from-sm {
@media (min-width: $from-sm) {
width: 50%;
.u-1\/2\@from-small {
@media (min-width: $from-small) {
width: span(1/2);
}
}

View File

@@ -1,11 +1,9 @@
import concatFiles from './tasks/concats.js';
import compileScripts from './tasks/scripts.js';
import compileStyles from './tasks/styles.js';
import compileSVGs from './tasks/svgs.js';
import bumpVersions from './tasks/versions.js';
import compileStyles from './tasks/styles.js' ;
import compileSVGs from './tasks/svgs.js' ;
concatFiles();
compileScripts();
compileStyles();
compileSVGs();
bumpVersions();

View File

@@ -1,25 +0,0 @@
/**
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json' with { type: 'json' };
import { merge } from '../utils/index.js';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json', {
with: { type: 'json' },
});
usrconfig = usrconfig.default;
merge(loconfig, usrconfig);
} catch (err) {
// do nothing
}
export default loconfig;
export {
loconfig,
};

View File

@@ -1,162 +0,0 @@
/**
* @file Retrieve the first available glob library.
*
* Note that options vary between libraries.
*
* Candidates:
*
* - {@link https://npmjs.com/package/tiny-glob tiny-glob} [1][5][6]
* - {@link https://npmjs.com/package/globby globby} [2][5]
* - {@link https://npmjs.com/package/fast-glob fast-glob} [3]
* - {@link https://npmjs.com/package/glob glob} [1][4][5]
*
* Notes:
*
* - [1] The library's function accepts only a single pattern.
* - [2] The library's function accepts only an array of patterns.
* - [3] The library's function accepts either a single pattern
* or an array of patterns.
* - [4] The library's function does not return a Promise but will be
* wrapped in a function that does return a Promise.
* - [5] The library's function will be wrapped in a function that
* supports a single pattern and an array of patterns.
* - [6] The library's function returns files and directories but will be
* preconfigured to return only files.
*/
import { promisify } from 'node:util';
/**
* @callback GlobFn
*
* @param {string|string[]} patterns - A string pattern
* or an array of string patterns.
* @param {object} options
*
* @returns {Promise<string[]>}
*/
/**
* @typedef {object} GlobOptions
*/
/**
* @type {GlobFn|undefined} The discovered glob function.
*/
let glob;
/**
* @type {string[]} A list of packages to attempt import.
*/
const candidates = [
'tiny-glob',
'globby',
'fast-glob',
'glob',
];
try {
glob = await importGlob();
} catch (err) {
// do nothing
}
/**
* @type {boolean} Whether a glob function was discovered (TRUE) or not (FALSE).
*/
const supportsGlob = (typeof glob === 'function');
/**
* Imports the first available glob function.
*
* @throws {TypeError} If no glob library was found.
*
* @returns {GlobFn}
*/
async function importGlob() {
for (let name of candidates) {
try {
let globModule = await import(name);
if (typeof globModule.default !== 'function') {
throw new TypeError(`Expected ${name} to be a function`);
}
/**
* Wrap the function to ensure
* a common pattern.
*/
switch (name) {
case 'tiny-glob':
/** [1][5] */
return createArrayableGlob(
/** [6] */
createPresetGlob(globModule.default, {
filesOnly: true
})
);
case 'globby':
/** [2][5] - If `patterns` is a string, wraps into an array. */
return (patterns, options) => globModule.default([].concat(patterns), options);
case 'glob':
/** [1][5] */
return createArrayableGlob(
/** [4] */
promisify(globModule.default)
);
default:
return globModule.default;
}
} catch (err) {
// swallow this error; skip to the next candidate.
}
}
throw new TypeError(
`No glob library was found, expected one of: ${candidates.join(', ')}`
);
}
/**
* Creates a wrapper function for the glob function
* to provide support for arrays of patterns.
*
* @param {function} globFn - The glob function.
*
* @returns {GlobFn}
*/
function createArrayableGlob(globFn) {
return (patterns, options) => {
/** [2] If `patterns` is a string, wraps into an array. */
patterns = [].concat(patterns);
const globs = patterns.map((pattern) => globFn(pattern, options));
return Promise.all(globs).then((files) => {
return [].concat.apply([], files);
});
};
}
/**
* Creates a wrapper function for the glob function
* to define new default options.
*
* @param {function} globFn - The glob function.
* @param {GlobOptions} presets - The glob function options to preset.
*
* @returns {GlobFn}
*/
function createPresetGlob(globFn, presets) {
return (patterns, options) => globFn(patterns, Object.assign({}, presets, options));
}
export default glob;
export {
glob,
supportsGlob,
};

View File

@@ -1,139 +0,0 @@
/**
* @file If available, returns the PostCSS Processor creator and
* any the Autoprefixer PostCSS plugin.
*/
/**
* @typedef {import('autoprefixer').autoprefixer.Options} AutoprefixerOptions
*/
/**
* @typedef {import('postcss').AcceptedPlugin} AcceptedPlugin
*/
/**
* @typedef {import('postcss').Postcss} Postcss
*/
/**
* @typedef {import('postcss').ProcessOptions} ProcessOptions
*/
/**
* @typedef {import('postcss').Processor} Processor
*/
/**
* @typedef {AcceptedPlugin[]} PluginList
*/
/**
* @typedef {object<string, AcceptedPlugin>} PluginMap
*/
/**
* @typedef {PluginList|PluginMap} PluginCollection
*/
/**
* @typedef {object} PostCSSOptions
*
* @property {ProcessOptions} processor - The `Processor#process()` options.
* @property {AutoprefixerOptions} autoprefixer - The `autoprefixer()` options.
*/
/**
* @type {Postcss|undefined} postcss - The discovered PostCSS function.
* @type {AcceptedPlugin|undefined} autoprefixer - The discovered Autoprefixer function.
*/
let postcss, autoprefixer;
try {
postcss = await import('postcss');
postcss = postcss.default;
autoprefixer = await import('autoprefixer');
autoprefixer = autoprefixer.default;
} catch (err) {
// do nothing
}
/**
* @type {boolean} Whether PostCSS was discovered (TRUE) or not (FALSE).
*/
const supportsPostCSS = (typeof postcss === 'function');
/**
* @type {PluginList} A list of supported plugins.
*/
const pluginsList = [
autoprefixer,
];
/**
* @type {PluginMap} A map of supported plugins.
*/
const pluginsMap = {
'autoprefixer': autoprefixer,
};
/**
* Attempts to create a PostCSS Processor with the given plugins and options.
*
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {PostCSSOptions} options - The PostCSS wrapper options.
*
* @returns {Processor|null}
*/
function createProcessor(pluginsListOrMap, options)
{
if (!postcss) {
return null;
}
const plugins = parsePlugins(pluginsListOrMap, options);
return postcss(plugins);
}
/**
* Parses the PostCSS plugins and options.
*
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {PostCSSOptions} options - The PostCSS wrapper options.
*
* @returns {PluginList}
*/
function parsePlugins(pluginsListOrMap, options)
{
if (Array.isArray(pluginsListOrMap)) {
return pluginsListOrMap;
}
/** @type {PluginList} */
const plugins = [];
for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) {
if (name in options) {
plugin = plugin[name](options[name]);
}
plugins.push(plugin);
}
return plugins;
}
export default postcss;
export {
autoprefixer,
createProcessor,
parsePlugins,
pluginsList,
pluginsMap,
postcss,
supportsPostCSS,
};

View File

@@ -1,9 +1,8 @@
import loconfig from '../helpers/config.js';
import glob, { supportsGlob } from '../helpers/glob.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import loconfig from '../utils/config.js';
import glob from '../utils/glob.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import resolve from '../utils/template.js';
import concat from 'concat';
import {
basename,
@@ -65,7 +64,7 @@ export const productionConcatFilesArgs = [
* @return {Promise}
*/
export default async function concatFiles(globOptions = null, concatOptions = null) {
if (supportsGlob) {
if (glob) {
if (globOptions == null) {
globOptions = productionGlobOptions;
} else if (
@@ -73,7 +72,7 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
globOptions !== developmentGlobOptions &&
globOptions !== productionGlobOptions
) {
globOptions = merge({}, defaultGlobOptions, globOptions);
globOptions = Object.assign({}, defaultGlobOptions, globOptions);
}
}
@@ -83,19 +82,10 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
concatOptions !== developmentConcatOptions &&
concatOptions !== productionConcatOptions
) {
concatOptions = merge({}, defaultConcatOptions, concatOptions);
concatOptions = Object.assign({}, defaultConcatOptions, concatOptions);
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.includes - One or more paths to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @return {Promise}
*/
loconfig.tasks.concats?.forEach(async ({
loconfig.tasks.concats.forEach(async ({
includes,
outfile,
label = null
@@ -108,25 +98,25 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
outfile = resolve(outfile);
if (supportsGlob && globOptions) {
includes = await glob(includes, globOptions);
let files;
if (glob && globOptions) {
files = await glob(includes, globOptions);
} else {
files = includes;
}
if (concatOptions.removeDuplicates) {
includes = includes.map((path) => normalize(path));
includes = [ ...new Set(includes) ];
files = files.map((path) => normalize(path));
files = [ ...new Set(files) ];
}
await concat(includes, outfile);
await concat(files, outfile);
if (includes.length) {
if (files.length) {
message(`${label} concatenated`, 'success', timeLabel);
} else {
message(`${label} is empty`, 'notice', timeLabel);

View File

@@ -1,8 +1,7 @@
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import resolve from '../utils/template.js';
import esbuild from 'esbuild';
import { basename } from 'node:path';
@@ -51,21 +50,10 @@ export default async function compileScripts(esBuildOptions = null) {
esBuildOptions !== developmentESBuildOptions &&
esBuildOptions !== productionESBuildOptions
) {
esBuildOptions = merge({}, defaultESBuildOptions, esBuildOptions);
esBuildOptions = Object.assign({}, defaultESBuildOptions, esBuildOptions);
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.includes - One or more paths to process.
* @param {string} [entry.outdir] - The directory to write to.
* @param {string} [entry.outfile] - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outdir or outfile name.
* @throws {TypeError} If outdir and outfile are missing.
* @return {Promise}
*/
loconfig.tasks.scripts?.forEach(async ({
loconfig.tasks.scripts.forEach(async ({
includes,
outdir = '',
outfile = '',
@@ -79,10 +67,6 @@ export default async function compileScripts(esBuildOptions = null) {
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
if (outdir) {

View File

@@ -1,17 +1,14 @@
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import {
createProcessor,
pluginsMap as postcssPluginsMap,
supportsPostCSS
} from '../helpers/postcss.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import postcss, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js';
import resolve from '../utils/template.js';
import { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import * as sass from 'sass';
import { PurgeCSS } from 'purgecss';
import { promisify } from 'node:util';
import sass from 'node-sass';
const sassRender = promisify(sass.render);
let postcssProcessor;
@@ -21,15 +18,15 @@ let postcssProcessor;
* @const {object} productionSassOptions - The predefined Sass options for production.
*/
export const defaultSassOptions = {
sourceMapIncludeSources: true,
omitSourceMapUrl: true,
sourceMap: true,
sourceMapContents: true,
};
export const developmentSassOptions = Object.assign({}, defaultSassOptions, {
style: 'expanded',
outputStyle: 'expanded',
});
export const productionSassOptions = Object.assign({}, defaultSassOptions, {
style: 'compressed',
outputStyle: 'compressed',
});
/**
@@ -56,12 +53,10 @@ export const productionPostCSSOptions = Object.assign({}, defaultPostCSSOptions
export const developmentStylesArgs = [
developmentSassOptions,
developmentPostCSSOptions,
false
];
export const productionStylesArgs = [
productionSassOptions,
productionPostCSSOptions,
true
];
/**
@@ -78,17 +73,17 @@ export const productionStylesArgs = [
* If `false`, PostCSS processing will be ignored.
* @return {Promise}
*/
export default async function compileStyles(sassOptions = null, postcssOptions = null, purge = true) {
export default async function compileStyles(sassOptions = null, postcssOptions = null) {
if (sassOptions == null) {
sassOptions = productionSassOptions;
} else if (
sassOptions !== developmentSassOptions &&
sassOptions !== productionSassOptions
) {
sassOptions = merge({}, defaultSassOptions, sassOptions);
sassOptions = Object.assign({}, defaultSassOptions, sassOptions);
}
if (supportsPostCSS) {
if (postcss) {
if (postcssOptions == null) {
postcssOptions = productionPostCSSOptions;
} else if (
@@ -96,20 +91,11 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
postcssOptions !== developmentPostCSSOptions &&
postcssOptions !== productionPostCSSOptions
) {
postcssOptions = merge({}, defaultPostCSSOptions, postcssOptions);
postcssOptions = Object.assign({}, defaultPostCSSOptions, postcssOptions);
}
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.infile - The file to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @return {Promise}
*/
loconfig.tasks.styles?.forEach(async ({
loconfig.tasks.styles.forEach(async ({
infile,
outfile,
label = null
@@ -123,11 +109,14 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
infile = resolve(infile);
outfile = resolve(outfile);
let result = sass.compile(infile, sassOptions);
let result = await sassRender(Object.assign({}, sassOptions, {
file: infile,
outFile: outfile,
}));
if (supportsPostCSS && postcssOptions) {
if (postcss && postcssOptions) {
if (typeof postcssProcessor === 'undefined') {
postcssProcessor = createProcessor(
postcssProcessor = createPostCSSProcessor(
postcssPluginsMap,
postcssOptions
);
@@ -153,12 +142,7 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
}
try {
await writeFile(outfile, result.css).then(() => {
// Purge CSS once file exists.
if (outfile && purge) {
purgeUnusedCSS(outfile, `${label || `${filestem}.css`}`);
}
});
await writeFile(outfile, result.css);
if (result.css) {
message(`${label || `${filestem}.css`} compiled`, 'success', timeLabel);
@@ -201,42 +185,30 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
};
/**
* Purge unused styles from CSS files.
* Creates a PostCSS Processor with the given plugins and options.
*
* @async
*
* @param {string} outfile - The path of a css file
* If missing the function stops.
* @param {string} label - The CSS file label or name.
* @return {Promise}
* @param {array<(function|object)>|object<string, (function|object)>} pluginsListOrMap -
* A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {object} options - The PostCSS options.
*/
async function purgeUnusedCSS(outfile, label) {
const contentFiles = loconfig.tasks.purgeCSS?.content;
if (!Array.isArray(contentFiles) || !contentFiles.length) {
return;
function createPostCSSProcessor(pluginsListOrMap, options)
{
let plugins;
if (Array.isArray(pluginsListOrMap)) {
plugins = pluginsListOrMap;
} else {
plugins = [];
for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) {
if (name in options) {
plugin = plugin[name](options[name]);
}
plugins.push(plugin);
}
}
label = label ?? basename(outfile);
const timeLabel = `${label} purged in`;
console.time(timeLabel);
const purgeCSSResults = await (new PurgeCSS()).purge({
content: contentFiles,
css: [ outfile ],
defaultExtractor: content => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [],
fontFaces: true,
keyframes: true,
safelist: {
// Keep all except .u-gc-* | .u-margin-* | .u-padding-*
standard: [ /^(?!.*\b(u-gc-|u-margin|u-padding)).*$/ ]
},
variables: true,
})
for (let result of purgeCSSResults) {
await writeFile(outfile, result.css)
message(`${label} purged`, 'chore', timeLabel);
}
return postcss(plugins);
}

View File

@@ -1,36 +1,20 @@
import loconfig from '../helpers/config.js';
import glob, { supportsGlob } from '../helpers/glob.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import { resolve as resolveTemplate } from '../helpers/template.js';
import { merge } from '../utils/index.js';
import {
basename,
dirname,
extname,
resolve,
} from 'node:path';
import commonPath from 'common-path';
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import resolve from '../utils/template.js';
import { basename } from 'node:path';
import mixer from 'svg-mixer';
import slugify from 'url-slug';
const basePath = loconfig?.paths?.svgs?.src
? resolve(loconfig.paths.svgs.src)
: null;
/**
* @const {object} defaultMixerOptions - The default shared Mixer options.
* @const {object} defaultMixerOptions - The default shared Mixer options.
* @const {object} developmentMixerOptions - The predefined Mixer options for development.
* @const {object} productionMixerOptions - The predefined Mixer options for production.
*/
export const defaultMixerOptions = {
spriteConfig: {
usages: false,
},
};
/**
* @const {object} developmentMixerOptions - The predefined Mixer options for development.
* @const {object} productionMixerOptions - The predefined Mixer options for production.
*/
export const developmentMixerOptions = Object.assign({}, defaultMixerOptions);
export const productionMixerOptions = Object.assign({}, defaultMixerOptions);
@@ -60,19 +44,10 @@ export default async function compileSVGs(mixerOptions = null) {
mixerOptions !== developmentMixerOptions &&
mixerOptions !== productionMixerOptions
) {
mixerOptions = merge({}, defaultMixerOptions, mixerOptions);
mixerOptions = Object.assign({}, defaultMixerOptions, mixerOptions);
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.includes - One or more paths to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @return {Promise}
*/
loconfig.tasks.svgs?.forEach(async ({
loconfig.tasks.svgs.forEach(async ({
includes,
outfile,
label = null
@@ -85,56 +60,10 @@ export default async function compileSVGs(mixerOptions = null) {
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
outfile = resolve(outfile);
includes = resolveTemplate(includes);
outfile = resolveTemplate(outfile);
if (supportsGlob && basePath) {
includes = await glob(includes);
includes = [ ...new Set(includes) ];
const common = commonPath(includes);
if (common.commonDir) {
common.commonDir = resolve(common.commonDir);
}
/**
* Generates the `<symbol id>` attribute and prefix any
* SVG files in subdirectories according to the paths
* common base path.
*
* Example for SVG source path `./assets/images/sprite`:
*
* | Path | ID |
* | ------------------------------------ | --------- |
* | `./assets/images/sprite/foo.svg` | `foo` |
* | `./assets/images/sprite/baz/qux.svg` | `baz-qux` |
*
* @param {string} path - The absolute path to the file.
* @param {string} [query=''] - A query string.
* @return {string} The symbol ID.
*/
mixerOptions.generateSymbolId = (path, query = '') => {
let dirName = dirname(path)
.replace(common.commonDir ?? basePath, '')
.replace(/^\/|\/$/, '')
.replace('/', '-');
if (dirName) {
dirName += '-';
}
const fileName = basename(path, extname(path));
const decodedQuery = decodeURIComponent(decodeURIComponent(query));
return `${dirName}${fileName}${slugify(decodedQuery)}`;
};
}
const result = await mixer(includes, {
...mixerOptions,
});
const result = await mixer(includes, mixerOptions);
await result.write(outfile);

View File

@@ -1,441 +0,0 @@
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import { randomBytes } from 'node:crypto';
import events from 'node:events';
import {
createReadStream,
createWriteStream,
} from 'node:fs';
import {
mkdir,
rename,
rm,
readFile,
writeFile,
} from 'node:fs/promises';
import {
basename,
dirname,
} from 'node:path';
import readline from 'node:readline';
export const REGEXP_SEMVER = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
/**
* @typedef {object} VersionOptions
* @property {string|number|null} prettyPrint - A string or number to insert
* white space into the output JSON string for readability purposes.
* @property {string} versionFormat - The version number format.
* @property {string|RegExp} versionKey - Either:
* - A string representing the JSON field name assign the version number to.
*
* Explicit:
*
* ```json
* "key": "json:version"
* ```
*
* Implicit:
*
* ```json
* "key": "version"
* ```
*
* - A `RegExp` object or regular expression string prefixed with `regexp:`.
*
* ```json
* "key": "regexp:(?<=^const ASSETS_VERSION = ')(?<version>\\d+)(?=';$)"
* ```
*
* ```js
* key: new RegExp('(?<=^const ASSETS_VERSION = ')(?<version>\\d+)(?=';$)')
* ```
*
* ```js
* key: /(?<=^const ASSETS_VERSION = ')(?<version>\d+)(?=';$)/
* ```
*/
/**
* @const {VersionOptions} defaultVersionOptions - The default shared version options.
* @const {VersionOptions} developmentVersionOptions - The predefined version options for development.
* @const {VersionOptions} productionVersionOptions - The predefined version options for production.
*/
export const defaultVersionOptions = {
prettyPrint: 4,
versionFormat: 'timestamp',
versionKey: 'version',
};
export const developmentVersionOptions = Object.assign({}, defaultVersionOptions);
export const productionVersionOptions = Object.assign({}, defaultVersionOptions);
/**
* @const {object} developmentVersionFilesArgs - The predefined `bumpVersion()` options for development.
* @const {object} productionVersionFilesArgs - The predefined `bumpVersion()` options for production.
*/
export const developmentVersionFilesArgs = [
developmentVersionOptions,
];
export const productionVersionFilesArgs = [
productionVersionOptions,
];
/**
* Bumps version numbers in file.
*
* @async
* @param {object} [versionOptions=null] - Customize the version options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function bumpVersions(versionOptions = null) {
if (versionOptions == null) {
versionOptions = productionVersionOptions;
} else if (
versionOptions !== developmentVersionOptions &&
versionOptions !== productionVersionOptions
) {
versionOptions = merge({}, defaultVersionOptions, versionOptions);
}
const queue = new Map();
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @param {?string} [entry.format] - The version number format.
* @param {?string} [entry.key] - The JSON field name assign the version number to.
* @param {?string|number} [entry.pretty] - The white space to use to format the JSON file.
* @return {Promise}
*/
loconfig.tasks.versions?.forEach(({
outfile,
label = null,
...options
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
options.pretty = (options.pretty ?? versionOptions.prettyPrint);
options.format = (options.format ?? versionOptions.versionFormat);
options.key = (options.key ?? versionOptions.versionKey);
if (queue.has(outfile)) {
queue.get(outfile).then(() => handleBumpVersion(outfile, label, options));
} else {
queue.set(outfile, handleBumpVersion(outfile, label, options));
}
});
};
/**
* Creates a formatted version number or string.
*
* @param {string} format - The version format.
* @param {?string} [oldValue] - The old version value.
* @return {string|number}
* @throws TypeError If the format or value are invalid.
*/
function createVersionNumber(format, oldValue = null) {
let [ type, modifier ] = format.split(':', 2);
switch (type) {
case 'hex':
case 'hexadecimal':
try {
modifier = Number.parseInt(modifier);
if (Number.isNaN(modifier)) {
modifier = 6;
}
return randomBytes(modifier).toString('hex');
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "hexadecimal"`,
{ cause: err }
);
}
case 'inc':
case 'increment':
try {
if (modifier === 'semver') {
return incrementSemVer(oldValue, [ 'buildmetadata', 'patch' ]);
}
return incrementNumber(oldValue, modifier);
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "increment"`,
{ cause: err }
);
}
case 'regex':
case 'regexp':
try {
return new RegExp(modifier);
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "regexp"`,
{ cause: err }
);
}
case 'timestamp':
return Date.now().valueOf();
}
throw new TypeError(
'Expected \'format\' to be either "timestamp", "increment", or "hexadecimal"'
);
}
/**
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @return {Promise}
*/
async function handleBumpVersion(outfile, label, options) {
const timeLabel = `${label} bumped in`;
console.time(timeLabel);
try {
options.key = parseVersionKey(options.key);
if (options.key instanceof RegExp) {
await handleBumpVersionWithRegExp(outfile, label, options);
} else {
await handleBumpVersionInJson(outfile, label, options);
}
message(`${label} bumped`, 'success', timeLabel);
} catch (err) {
message(`Error bumping ${label}`, 'error');
message(err);
notification({
title: `${label} bumping failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
}
/**
* Changes the version number for the provided JSON key in file.
*
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @param {string} options.key
* @return {Promise}
*/
async function handleBumpVersionInJson(outfile, label, options) {
outfile = resolve(outfile);
let json;
try {
json = JSON.parse(await readFile(outfile, { encoding: 'utf8' }));
} catch (err) {
json = {};
message(`${label} is a new file`, 'notice');
await mkdir(dirname(outfile), { recursive: true });
}
json[options.key] = createVersionNumber(options.format, json?.[options.key]);
return await writeFile(
outfile,
JSON.stringify(json, null, options.pretty),
{ encoding: 'utf8' }
);
}
/**
* Changes the version number for the provided RegExp in file.
*
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @param {RegExp} options.key
* @return {Promise}
*/
async function handleBumpVersionWithRegExp(outfile, label, options) {
outfile = resolve(outfile);
const bckfile = `${outfile}~`;
await rename(outfile, bckfile);
try {
const rl = readline.createInterface({
input: createReadStream(bckfile),
});
let newVersion = null;
const writeStream = createWriteStream(outfile, { encoding: 'utf8' });
rl.on('line', (line) => {
const found = line.match(options.key);
if (found) {
const groups = (found.groups ?? {});
const oldVersion = (groups.build ?? groups.version ?? found[1] ?? found[0]);
const newVersion = createVersionNumber(options.format, oldVersion);
const replacement = found[0].replace(oldVersion, newVersion);
line = line.replace(found[0], replacement);
}
writeStream.write(line + "\n");
});
await events.once(rl, 'close');
await rm(bckfile);
} catch (err) {
await rm(outfile, { force: true });
await rename(bckfile, outfile);
throw err;
}
}
/**
* Increments the given integer.
*
* @param {string|int} value - The number to increment.
* @param {string|int} [increment=1] - The amount to increment by.
* @return {int}
* @throws TypeError If the version number is invalid.
*/
function incrementNumber(value, increment = 1) {
const version = Number.parseInt(value);
if (Number.isNaN(version)) {
throw new TypeError(
`Expected an integer version number, received [${value}]`
);
}
increment = Number.parseInt(increment);
if (Number.isNaN(increment)) {
throw new TypeError(
'Expected an integer increment number'
);
}
return (version + increment);
}
/**
* Increments the given SemVer version number.
*
* @param {string} value - The version to mutate.
* @param {string|string[]} [target] - The segment to increment, one of:
* 'major', 'minor', 'patch', ~~'prerelease'~~, 'buildmetadata'.
* @param {string|int} [increment=1] - The amount to increment by.
* @return {string}
* @throws TypeError If the version or target are invalid.
*/
function incrementSemVer(value, target = 'patch', increment = 1) {
const found = value.match(REGEXP_SEMVER);
if (!found) {
throw new TypeError(
`Expected a SemVer version number, received [${value}]`
);
}
if (Array.isArray(target)) {
for (const group of target) {
if (found.groups[group] != null) {
target = group;
break;
}
}
}
if (!target || !found.groups[target]) {
throw new TypeError(
`Expected a supported SemVer segment, received [${target}]`
);
}
const segments = {
'major': '',
'minor': '.',
'patch': '.',
'prerelease': '-',
'buildmetadata': '+',
};
let replacement = '';
for (const [ segment, delimiter ] of Object.entries(segments)) {
if (found.groups?.[segment] != null) {
const newVersion = (segment === target)
? incrementNumber(found.groups[segment], increment)
: found.groups[segment];
replacement += `${delimiter}${newVersion}`;
}
}
return value.replace(found[0], replacement);
}
/**
* Parses the version key.
*
* @param {*} key - The version key.
* @return {string|RegExp}
*/
function parseVersionKey(key) {
if (key instanceof RegExp) {
return key;
}
if (typeof key !== 'string') {
throw new TypeError(
'Expected \'key\' to be either a string or a RegExp'
);
}
const delimiter = key.indexOf(':');
if (delimiter === -1) {
// Assumes its a JSON key
return key;
}
const type = key.slice(0, delimiter);
const value = key.slice(delimiter + 1);
switch (type) {
case 'json':
return value;
case 'regex':
case 'regexp':
return new RegExp(value);
}
throw new TypeError(
'Expected \'key\' type to be either "json" or "regexp"'
);
}

64
build/utils/config.js Normal file
View File

@@ -0,0 +1,64 @@
/**
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json');
usrconfig = usrconfig.default;
merge(loconfig, usrconfig);
} catch (err) {
// do nothing
}
export default loconfig;
/**
* Creates a new object with all nested object properties
* merged into it recursively.
*
* @param {object} target - The target object.
* @param {object[]} ...sources - The source object(s).
* @throws {TypeError} If the target and source are the same.
* @return {object} Returns the `target` object.
*/
export function merge(target, ...sources) {
for (const source of sources) {
if (target === source) {
throw new TypeError(
'Cannot merge, target and source are the same'
);
}
for (const key in source) {
if (source[key] != null) {
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
merge(target[key], source[key]);
continue;
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
target[key] = target[key].concat(source[key]);
continue;
}
}
target[key] = source[key];
}
}
return target;
}
/**
* Determines whether the passed value is an `Object`.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is an `Object`,
* otherwise `false`.
*/
function isObjectLike(value) {
return (value != null && typeof value === 'object');
}

95
build/utils/glob.js Normal file
View File

@@ -0,0 +1,95 @@
/**
* @file Retrieve the first available glob library.
*
* Note that options vary between libraries.
*
* Candidates:
*
* - {@link https://npmjs.com/package/tiny-glob tiny-glob}
* - {@link https://npmjs.com/package/globby globby}
* - {@link https://npmjs.com/package/fast-glob fast-glob}
* - {@link https://npmjs.com/package/glob glob}
*/
import { promisify } from 'node:util';
/**
* @type {string[]} A list of packages to attempt import.
*/
const candidates = [
'tiny-glob',
'globby',
'fast-glob',
'glob',
];
let glob;
try {
glob = await importGlob();
} catch (err) {
// do nothing
}
export default glob;
/**
* Imports the first available glob function.
*
* @throws {TypeError} If no glob library was found.
* @return {function}
*/
async function importGlob() {
let glob, module;
for (let name of candidates) {
try {
module = await import(name);
if (typeof module.default !== 'function') {
throw new TypeError(`Expected ${name} to be a function`);
}
/**
* Wrap the function to ensure
* a common pattern.
*/
switch (name) {
case 'tiny-glob':
return createArrayableGlob(module.default, {
filesOnly: true
});
case 'glob':
return promisify(module.default);
default:
return module.default;
}
} catch (err) {
// swallow this error; skip to the next candidate.
}
}
throw new TypeError(
`No glob library was found, expected one of: ${candidates.join(', ')}`
);
}
/**
* Creates a wrapper function for the glob function
* to provide support for arrays of patterns.
*
* @param {function} glob - The glob function.
* @param {object} options - The glob options.
* @return {function}
*/
function createArrayableGlob(glob, options) {
return (patterns, options) => {
const globs = patterns.map((pattern) => glob(pattern, options));
return Promise.all(globs).then((files) => {
return [].concat.apply([], files);
});
};
}

View File

@@ -1,115 +0,0 @@
/**
* @file Provides generic functions and constants.
*/
/**
* @type {RegExp} - Match all special characters.
*/
const regexUnescaped = /[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g;
/**
* Quotes regular expression characters.
*
* @param {string} str - The input string.
* @return {string} Returns the quoted (escaped) string.
*/
function escapeRegExp(str) {
return str.replace(regexUnescaped, '\\$&');
}
/**
* Creates a new object with all nested object properties
* concatenated into it recursively.
*
* Nested keys are flattened into a property path:
*
* ```js
* {
* a: {
* b: {
* c: 1
* }
* },
* d: 1
* }
* ```
*
* ```js
* {
* "a.b.c": 1,
* "d": 1
* }
* ```
*
* @param {object} input - The object to flatten.
* @param {string} prefix - The parent key prefix.
* @param {object} target - The object that will receive the flattened properties.
* @return {object} Returns the `target` object.
*/
function flatten(input, prefix, target = {}) {
for (const key in input) {
const field = (prefix ? prefix + '.' + key : key);
if (isObjectLike(input[key])) {
flatten(input[key], field, target);
} else {
target[field] = input[key];
}
}
return target;
}
/**
* Determines whether the passed value is an `Object`.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is an `Object`,
* otherwise `false`.
*/
function isObjectLike(value) {
return (value != null && typeof value === 'object');
}
/**
* Creates a new object with all nested object properties
* merged into it recursively.
*
* @param {object} target - The target object.
* @param {object[]} ...sources - The source object(s).
* @throws {TypeError} If the target and source are the same.
* @return {object} Returns the `target` object.
*/
function merge(target, ...sources) {
for (const source of sources) {
if (target === source) {
throw new TypeError(
'Cannot merge, target and source are the same'
);
}
for (const key in source) {
if (source[key] != null) {
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
merge(target[key], source[key]);
continue;
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
target[key] = target[key].concat(source[key]);
continue;
}
}
target[key] = source[key];
}
}
return target;
}
export {
escapeRegExp,
flatten,
isObjectLike,
merge,
regexUnescaped,
};

View File

@@ -11,16 +11,12 @@ import kleur from 'kleur';
* @param {string} [type] - The type of message.
* @param {string} [timerID] - The console time label to output.
*/
function message(text, type, timerID) {
export default function message(text, type, timerID) {
switch (type) {
case 'success':
console.log('✅ ', kleur.bgGreen().black(text));
break;
case 'chore':
console.log('🧹 ', kleur.bgGreen().black(text));
break;
case 'notice':
console.log(' ', kleur.bgBlue().black(text));
break;
@@ -52,10 +48,4 @@ function message(text, type, timerID) {
}
console.log('');
}
export default message;
export {
message,
};

View File

@@ -16,7 +16,7 @@ import notifier from 'node-notifier';
* @param {function} callback - The notification callback.
* @return {void}
*/
function notification(options, callback) {
export default function notification(options, callback) {
if (typeof options === 'string') {
options = {
message: options
@@ -42,10 +42,4 @@ function notification(options, callback) {
}
notifier.notify(options, callback);
}
export default notification;
export {
notification,
};

27
build/utils/postcss.js Normal file
View File

@@ -0,0 +1,27 @@
/**
* @file If available, returns the PostCSS Processor creator and
* any the Autoprefixer PostCSS plugin.
*/
let postcss, autoprefixer;
try {
postcss = await import('postcss');
postcss = postcss.default;
autoprefixer = await import('autoprefixer');
autoprefixer = autoprefixer.default;
} catch (err) {
// do nothing
}
export default postcss;
export const pluginsList = [
autoprefixer,
];
export const pluginsMap = {
'autoprefixer': autoprefixer,
};
export {
autoprefixer
};

View File

@@ -3,10 +3,6 @@
*/
import loconfig from './config.js';
import {
escapeRegExp,
flatten
} from '../utils/index.js';
const templateData = flatten({
paths: loconfig.paths
@@ -26,7 +22,7 @@ const templateData = flatten({
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {*} Returns the transformed value.
*/
function resolve(input, data = templateData) {
export default function resolve(input, data = templateData) {
switch (typeof input) {
case 'string': {
return resolveValue(input, data);
@@ -60,7 +56,7 @@ function resolve(input, data = templateData) {
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {string} Returns the translated string.
*/
function resolveValue(input, data = templateData) {
export function resolveValue(input, data = templateData) {
const tags = [];
if (data !== templateData) {
@@ -97,9 +93,55 @@ function resolveValue(input, data = templateData) {
});
}
export default resolve;
/**
* Creates a new object with all nested object properties
* concatenated into it recursively.
*
* Nested keys are flattened into a property path:
*
* ```js
* {
* a: {
* b: {
* c: 1
* }
* },
* d: 1
* }
* ```
*
* ```js
* {
* "a.b.c": 1,
* "d": 1
* }
* ```
*
* @param {object} input - The object to flatten.
* @param {string} prefix - The parent key prefix.
* @param {object} target - The object that will receive the flattened properties.
* @return {object} Returns the `target` object.
*/
function flatten(input, prefix, target = {}) {
for (let key in input) {
let field = (prefix ? prefix + '.' + key : key);
export {
resolve,
resolveValue,
};
if (typeof input[key] === 'object') {
flatten(input[key], field, target);
} else {
target[field] = input[key];
}
}
return target;
}
/**
* Quotes regular expression characters.
*
* @param {string} str - The input string.
* @return {string} Returns the quoted (escaped) string.
*/
function escapeRegExp(str) {
return str.replace(/[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g, '\\$&');
}

View File

@@ -2,11 +2,10 @@ import concatFiles, { developmentConcatFilesArgs } from './tasks/concats.js';
import compileScripts, { developmentScriptsArgs } from './tasks/scripts.js';
import compileStyles, { developmentStylesArgs } from './tasks/styles.js' ;
import compileSVGs, { developmentSVGsArgs } from './tasks/svgs.js';
import loconfig from './helpers/config.js';
import message from './helpers/message.js';
import notification from './helpers/notification.js';
import resolve from './helpers/template.js';
import { merge } from './utils/index.js';
import loconfig, { merge } from './utils/config.js';
import message from './utils/message.js';
import notification from './utils/notification.js';
import resolve from './utils/template.js';
import browserSync from 'browser-sync';
import { join } from 'node:path';
@@ -70,18 +69,16 @@ function configureServer(server, { paths, tasks }) {
});
// Watch source concats
if (tasks.concats?.length) {
server.watch(
resolve(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
)
server.watch(
resolve(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
)
).on('change', () => {
concatFiles(...developmentConcatFilesArgs);
});
}
)
).on('change', () => {
concatFiles(...developmentConcatFilesArgs);
});
// Watch source styles
server.watch(
@@ -116,8 +113,7 @@ function createServerOptions({
}) {
const config = {
open: false,
notify: false,
ghostMode: false
notify: false
};
// Resolve the URL for the BrowserSync server

View File

@@ -15,7 +15,6 @@
* [`scripts`](#scripts)
* [`styles`](#styles)
* [`svgs`](#svgs)
* [`versions`](#versions)
---
@@ -29,9 +28,7 @@ Learn more about the boilerplate's [tasks](#tasks) below.
Make sure you have the following installed:
* [Node] — at least 14.17, the latest LTS is recommended.
* [NPM] — at least 8.0, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
* [NPM] — at least 6.0, the latest LTS is recommended.
```sh
# Switch to recommended Node version from .nvmrc
@@ -265,8 +262,8 @@ See [`scripts.js`](../build/tasks/scripts.js) for details.
### `styles`
A wrapper around [sass] (with optional support for [Autoprefixer]
via [PostCSS]) for compiling and minifying Sass into CSS.
A wrapper around [node-sass] (and optionally [Autoprefixer] via [PostCSS])
for compiling and minifying Sass into CSS.
By default, [PostCSS] and [Autoprefixer] are installed with the boilerplate.
@@ -301,9 +298,6 @@ Example:
See [`styles.js`](../build/tasks/styles.js) for details.
The task also supports [PurgeCSS] to remove unused CSS.
See the [documentation on our Grid System](grid.md#build-tasks) for details.
### `svgs`
A wrapper around [SVG Mixer] for transforming and minifying SVG files
@@ -334,86 +328,6 @@ Example:
See [`svgs.js`](../build/tasks/svgs.js) for details.
### `versions`
A task to create and update values for use in versioning assets.
Can generate a hexadecimal value (using random bytes), use the current timestamp,
or increment a number.
Example:
```json
{
"versions": [
{
"format": "timestamp",
"key": "now",
"outfile": "./assets.json"
},
{
"format": "hex:8",
"key": "hex",
"outfile": "./assets.json"
},
{
"format": "inc:semver",
"key": "inc",
"outfile": "./assets.json"
}
]
}
```
```json
{
"now": 1665071717350,
"hex": "6ef54181c4ba",
"hex": "1.0.2"
}
```
The task supports replacing the value of a data key in a JSON file or replacing
a string in a file using a [regular expression](RegExp).
* Explicit JSON field name:
```json
{
"key": "json:version"
}
```
* Implicit JSON field name:
```json
{
"key": "version"
}
```
The regular expression can be a `RegExp` object or a pattern prefixed with `regexp:`.
* ```json
{
"key": "regexp:(?<=^const ASSETS_VERSION = ')(?<build>\\d+)(?=';$)"
}
```
* ```js
{
key: new RegExp('(?<=^const ASSETS_VERSION = ')(?<version>\\d+)(?=';$)')
}
```
* ```js
{
key: /^ \* Version: +(?:.+?)\+(.+?)$/
}
```
The regular expression pattern will match the first occurrence and replace
the first match in the following order: `build` (named capture), `version`
(named capture), `1` (first capture), or `0` (whole match).
See [`versions.js`](../build/tasks/versions.js) for details.
[Autoprefixer]: https://npmjs.com/package/autoprefixer
[BrowserSync]: https://npmjs.com/package/browser-sync
[concat]: https://npmjs.com/package/concat
@@ -422,11 +336,9 @@ See [`versions.js`](../build/tasks/versions.js) for details.
[glob]: https://npmjs.com/package/glob
[globby]: https://npmjs.com/package/globby
[Node]: https://nodejs.org/
[sass]: https://npmjs.com/package/sass
[node-sass]: https://npmjs.com/package/node-sass
[NPM]: https://npmjs.com/
[NVM]: https://github.com/nvm-sh/nvm
[PostCSS]: https://npmjs.com/package/postcss
[PurgeCSS]: https://purgecss.com/
[RegExp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[SVG Mixer]: https://npmjs.com/package/svg-mixer
[tiny-glob]: https://npmjs.com/package/tiny-glob

View File

@@ -1,109 +0,0 @@
# Grid system
* [Architectures](#architecture)
* [Build tasks](#build-tasks)
* [Configuration](#configuration)
* [Usage](#usage)
* [Example](#example)
## Architecture
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.
The following styles are needed to work properly:
* [`o-grid`](../assets/styles/objects/_grid.scss) — Object file where the default grid styles are set such as column numbers, modifiers, and options.
* [`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.
### Build tasks
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.
#### Configuration
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.
Example of a Charcoal project:
```jsonc
"purgeCSS": {
"content": [
"./views/app/template/**/*.mustache",
"./src/App/Template/*.php",
"./assets/scripts/**/*" // use case: `el.classList.add('u-gc-1/2')`
]
}
```
## Usage
The first step is to set intial SCSS values in the following files :
- [`settings/_config.scss`](../assets/styles/settings/_config.scss)
```scss
// Grid
// ==========================================================================
$base-column-nb: 12;
$base-column-gap: $unit-small;
```
You can create multiple column layouts depending on media queries.
- [`objects/_grid.scss`](../assets/styles/objects/_grid.scss)
```scss
.o-grid {
display: grid;
width: 100%;
margin: 0;
padding: 0;
list-style: none;
// ==========================================================================
// Cols
// ==========================================================================
&.-col-#{$base-column-nb} {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
&.-col-4 {
grid-template-columns: repeat(4, 1fr);
}
&.-col-#{$base-column-nb}\@from-md {
@media (min-width: $from-md) {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
}
// …
```
### Example
The following layout has 4 columns at `>=999px` and 12 columns at `<1000px`.
```html
<div class="o-container">
<h1 class="c-heading -h1">Hello</h1>
<div class="o-grid -col-4 -col-12@from-medium -gutters">
<div class="o-grid_item u-gc-1/8@from-medium">
<h2 class="c-heading -h2">This grid has 4 columns and 12 columns from `medium` MQ</h2>
</div>
<div class="o-grid_item u-gc-1/5@from-medium">
<p>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?</p>
</div>
<div class="o-grid_item u-gc-5/9@from-medium">
<p>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?</p>
</div>
<div class="o-grid_item u-gc-9/13@from-medium">
<p>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?</p>
</div>
</div>
</div>
```
[PurgeCSS]: https://purgecss.com/

View File

@@ -80,10 +80,10 @@ Learn about [namespacing](https://csswizardry.com/2015/03/more-transparent-ui-co
}
.c-block_heading {
@media (max-width: $to-md) {
@media (max-width: $to-medium) {
.c-block.-large & {
margin-bottom: rem(40px);
}
}
}
}
```
@@ -181,7 +181,10 @@ detection and smooth scrolling with parallax.
```js
import LocomotiveScroll from 'locomotive-scroll';
this.scroll = new LocomotiveScroll({})
this.scroll = new LocomotiveScroll({
el: this.el,
smooth: true
});
````
Learn more about [Locomotive Scroll][locomotive-scroll].

View File

@@ -15,11 +15,11 @@
"dest": "./www/assets/scripts"
},
"svgs": {
"src": "./assets/svgs",
"src": "./assets/images/sprite",
"dest": "./www/assets/images"
},
"views": {
"src": "./www/"
"src": "./views/boilerplate/template"
}
},
"tasks": {
@@ -56,17 +56,6 @@
],
"outfile": "{% paths.svgs.dest %}/sprite.svg"
}
],
"purgeCSS": {
"content": [
"./www/**/*.html",
"./assets/scripts/**/*"
]
},
"versions": [
{
"outfile": "./assets.json"
}
]
}
}

13546
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,42 +6,30 @@
"author": "Locomotive <info@locomotive.ca>",
"type": "module",
"engines": {
"node": ">=20",
"npm": ">=10"
"node": ">=14.17",
"npm": ">=6.0"
},
"scripts": {
"start": "node --no-warnings build/watch.js",
"build": "node --no-warnings build/build.js"
"start": "node --experimental-json-modules --no-warnings build/watch.js",
"build": "node --experimental-json-modules --no-warnings build/build.js"
},
"dependencies": {
"locomotive-scroll": "^5.0.0-beta.21",
"locomotive-scroll": "^4.1.4",
"modujs": "^1.4.2",
"modularload": "^1.2.6"
"modularload": "^1.2.6",
"normalize.css": "^8.0.1",
"svg4everybody": "^2.1.9"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"browser-sync": "^3.0.2",
"common-path": "^1.0.1",
"autoprefixer": "^10.4.4",
"browser-sync": "^2.27.9",
"concat": "^1.0.3",
"esbuild": "^0.24.2",
"kleur": "^4.1.5",
"esbuild": "^0.14.27",
"kleur": "^4.1.4",
"node-notifier": "^10.0.1",
"postcss": "^8.4.49",
"purgecss": "^7.0.2",
"sass": "^1.79.5",
"svg-mixer": "^2.4.0",
"node-sass": "^7.0.1",
"postcss": "^8.4.12",
"svg-mixer": "^2.3.14",
"tiny-glob": "^0.2.9"
},
"overrides": {
"browser-sync": {
"ua-parser-js": "^1.0.33"
},
"svg-mixer": {
"micromatch": "^4.0.8",
"postcss": "^8.4.49"
},
"svg-mixer-utils": {
"anymatch": "^3.1.3"
}
}
}

3702
www/assets/scripts/app.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More