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

3 Commits

Author SHA1 Message Date
Chauncey McAskill
edf4a51c18 Improve SVGO configset and SVGs task 2024-01-29 14:57:13 -05:00
Chauncey McAskill
d044be872d Add NPM script for SVGO with default config 2024-01-29 14:53:50 -05:00
Chauncey McAskill
b58487acbf Add optional support for SVGO
Added:
- NPM dependency svgo v3.2.0
- Helper 'svgo.js' to use dynamic import to fetch SVGO, if available.

Changed:
- 'svgs.js' to conditionally process spritesheets with SVGO if it's available.
2024-01-29 14:53:49 -05:00
54 changed files with 6834 additions and 3341 deletions

9
.gitignore vendored
View File

@@ -3,11 +3,4 @@ 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
.prettierrc

2
.nvmrc
View File

@@ -1 +1 @@
v20
v20.10

View File

@@ -23,8 +23,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 17.9, 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.

3
assets.json Normal file
View File

@@ -0,0 +1,3 @@
{
"version": 1706558057916
}

View File

@@ -10,73 +10,7 @@ const app = new modular({
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() {
const setViewportSizes = () => {
// Document styles
const documentStyles = document.documentElement.style;
@@ -91,10 +25,10 @@ function setViewportSizes() {
}
// Viewport height
const height = window.innerHeight;
const svh = document.documentElement.clientHeight * 0.01;
documentStyles.setProperty('--svh', `${svh}px`);
const dvh = window.innerHeight * 0.01;
const dvh = height * 0.01;
documentStyles.setProperty('--dvh', `${dvh}px`);
if (document.body) {
@@ -119,9 +53,6 @@ function setViewportSizes() {
}
}
////////////////
// Execute
////////////////
window.addEventListener('load', () => {
const $style = document.getElementById('main-css');
@@ -135,3 +66,46 @@ window.addEventListener('load', () => {
console.warn('The "main-css" stylesheet not found');
}
});
function init() {
globals();
setViewportSizes();
app.init(app);
$html.classList.add(CSS_CLASS.LOADED);
$html.classList.add(CSS_CLASS.READY);
$html.classList.remove(CSS_CLASS.LOADING);
// Bind window resize event with default vars
const resizeEndEvent = new CustomEvent(CUSTOM_EVENT.RESIZE_END);
window.addEventListener('resize', () => {
setViewportSizes();
debounce(() => {
window.dispatchEvent(resizeEndEvent);
}, 200, false)
})
window.addEventListener('orientationchange', () => {
setViewportSizes();
})
/**
* Eagerly load the following fonts.
*/
if (isFontLoadingAPIAvailable) {
loadFonts(FONT.EAGER, ENV.IS_DEV).then((eagerFonts) => {
$html.classList.add(CSS_CLASS.FONTS_LOADED);
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();
}
});
}
}

View File

@@ -1,3 +1,4 @@
import svg4everybody from 'svg4everybody';
import { ENV } from './config';
// Dynamic imports for development mode only
@@ -10,6 +11,11 @@ let gridHelper;
})();
export default function () {
/**
* Use external SVG spritemaps
*/
svg4everybody();
/**
* Add grid helper
*/

View File

@@ -27,7 +27,7 @@ $input-icon-color: 424242; // No #
.c-form_input {
padding: rem(10px);
border: 1px solid lightgray;
background-color: colorCode(lightest);
background-color: color(lightest);
&:hover {
border-color: darkgray;
@@ -71,7 +71,7 @@ $checkbox-icon-color: $input-icon-color;
}
&::before {
background-color: colorCode(lightest);
background-color: color(lightest);
border: 1px solid lightgray;
}

View File

@@ -2,78 +2,30 @@
// 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;
margin-bottom: rem(30px);
&.-h1 {
@include heading-h1;
font-size: var(--font-size-h1);
}
&.-h2 {
@include heading-h2;
font-size: var(--font-size-h2);
}
&.-h3 {
@include heading-h3;
font-size: var(--font-size-h3);
}
&.-h4 {
@include heading-h4;
font-size: var(--font-size-h4);
}
&.-h5 {
@include heading-h5;
font-size: var(--font-size-h5);
}
&.-h6 {
@include heading-h6;
font-size: var(--font-size-h6);
}
}

View File

@@ -25,7 +25,7 @@
position: absolute;
top: 0;
right: 0;
background-color: colorCode(darkest);
background-color: color(darkest);
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

@@ -20,26 +20,34 @@ html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: $to-sm) {
@media (max-width: $to-small) {
font-size: $font-size - 2px;
}
@media (min-width: $from-sm) and (max-width: $to-lg) {
@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-lg) and (max-width: $to-2xl) {
font-size: $font-size;
@media (min-width: $from-large) and (max-width: $to-huge) {
font-size: $font-size; // [1]
}
@media (min-width: $from-2xl) and (max-width: $to-3xl) {
@media (min-width: $from-huge) and (max-width: $to-gigantic) {
font-size: $font-size + 1px;
}
@media (min-width: $from-3xl) {
@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;
}

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,34 @@
// ==========================================================================
// 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,87 @@
// ==========================================================================
// 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: $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,52 @@
// ==========================================================================
// 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

@@ -20,24 +20,32 @@
// 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.spacers";
@import "settings/config.timings";
@import "settings/config.zindexes";
@import "settings/config";
@import "settings/config.variables";
// Generic
// ==========================================================================
@import "node_modules/normalize.css/normalize";
@import "generic/generic";
@import "generic/media";
@import "generic/form";
@import "generic/button";
// Vendors
// ==========================================================================
@import "../../node_modules/locomotive-scroll/dist/locomotive-scroll";
@import "node_modules/locomotive-scroll/dist/locomotive-scroll";
// Elements
// ==========================================================================
@import "elements/normalize";
@import "elements/document";
// Objects
@@ -54,7 +62,6 @@
// ==========================================================================
@import "components/heading";
@import "components/text";
@import "components/button";
@import "components/form";

View File

@@ -45,8 +45,8 @@
grid-template-columns: repeat(4, 1fr);
}
&.-col-#{$base-column-nb}\@from-md {
@media (min-width: $from-md) {
&.-col-#{$base-column-nb}\@from-medium {
@media (min-width: $from-medium) {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
}

View File

@@ -48,7 +48,7 @@
// // Logo
// .svg-logo {
// --icon-width: #{rem(100px)};
// --icon-ratio: math.div(20, 30); // width/height based on svg viewBox
// --icon-ratio: 20/30; // width/height based on svg viewBox
// // Sizes
// .o-icon.-big & {

View File

@@ -6,16 +6,15 @@
// ==========================================================================
$breakpoints: (
"2xs": 340px,
"xs": 500px,
"sm": 700px,
"md": 1000px,
"lg": 1200px,
"xl": 1400px,
"2xl": 1600px,
"3xl": 1800px,
"4xl": 2000px,
"5xl": 2400px
"tiny": 500px,
"small": 700px,
"medium": 1000px,
"large": 1200px,
"big": 1400px,
"huge": 1600px,
"enormous": 1800px,
"gigantic": 2000px,
"colossal": 2400px
);
// Functions
@@ -76,17 +75,21 @@ $breakpoints: (
// 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;
$from-tiny: map-get($breakpoints, "tiny") !default;
$to-tiny: map-get($breakpoints, "tiny") - 1 !default;
$from-small: map-get($breakpoints, "small") !default;
$to-small: map-get($breakpoints, "small") - 1 !default;
$from-medium: map-get($breakpoints, "medium") !default;
$to-medium: map-get($breakpoints, "medium") - 1 !default;
$from-large: map-get($breakpoints, "large") !default;
$to-large: map-get($breakpoints, "large") - 1 !default;
$from-big: map-get($breakpoints, "big") !default;
$to-big: map-get($breakpoints, "big") - 1 !default;
$from-huge: map-get($breakpoints, "huge") !default;
$to-huge: map-get($breakpoints, "huge") - 1 !default;
$from-enormous: map-get($breakpoints, "enormous") !default;
$to-enormous: map-get($breakpoints, "enormous") - 1 !default;
$from-gigantic: map-get($breakpoints, "gigantic") !default;
$to-gigantic: map-get($breakpoints, "gigantic") - 1 !default;
$from-colossal: map-get($breakpoints, "colossal") !default;
$to-colossal: map-get($breakpoints, "colossal") - 1 !default;

View File

@@ -1,5 +1,3 @@
@use 'sass:color';
// ==========================================================================
// Settings / Config / Colors
// ==========================================================================
@@ -20,7 +18,7 @@ $colors: (
//
// ```scss
// .c-box {
// color: colorCode(primary);
// color: color(primary);
// }
// ```
//
@@ -28,7 +26,7 @@ $colors: (
// @param {number} $alpha - The alpha for the color value.
// @return {color}
@function colorCode($key, $alpha: 1) {
@function color($key, $alpha: 1) {
@if not map-has-key($colors, $key) {
@error "Unknown '#{$key}' in $colors.";
}
@@ -46,13 +44,13 @@ $colors: (
// ==========================================================================
// Link
$color-link: colorCode(primary);
$color-link-focus: colorCode(primary);
$color-link-hover: color.adjust(colorCode(primary), $lightness: -10%);
$color-link: color(primary);
$color-link-focus: color(primary);
$color-link-hover: darken(color(primary), 10%);
// Selection
$color-selection-text: colorCode(darkest);
$color-selection-background: colorCode(lightest);
$color-selection-text: color(darkest);
$color-selection-background: color(lightest);
// Socials
$color-facebook: #3B5998;

View File

@@ -17,7 +17,7 @@ $assets-path: "../" !default;
// Base
$font-size: 16px;
$line-height: math.div(24px, $font-size);
$font-color: colorCode(darkest);
$font-color: color(darkest);
// Weights
$font-weight-light: 300;
@@ -32,13 +32,12 @@ $easing: ease("power2.out");
// Spacing Units
// =============================================================================
$unit: 60px;
$unit-small: 20px;
$vw-viewport: 1440;
$unit: 60px;
$unit-small: 20px;
// Container
// ==========================================================================
$padding: $unit;
$padding: $unit;
// Grid
// ==========================================================================

View File

@@ -0,0 +1,40 @@
// ==========================================================================
// Settings / Config / Spacers
// ==========================================================================
// Spacers
// ==========================================================================
$spacers: (
'gutter': var(--grid-gutter),
'xs': #{vh(5)},
'sm': #{vh(7.5)},
'md': #{vh(10)},
'lg': #{vh(12.5)},
'xl': #{vh(15)},
);
// Function
// ==========================================================================
// Returns spacer.
//
// ```scss
// .c-box {
// margin-top: spacer(gutter);
// }
// ```
//
// @param {string} $key - The spacer key in $spacers.
// @param {number} $multiplier - The multiplier of the spacer value.
// @return {size}
@function spacer($spacer: $spacer-default, $multiplier: 1) {
@if not map-has-key($spacers, $spacer) {
@error "Unknown master spacer: #{$spacer}";
}
$index: map-get($spacers, $spacer);
@return calc(#{$index} * #{$multiplier});
}

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,20 +1,23 @@
// ==========================================================================
// Settings / Config / Speeds
// Settings / Config / Timings
// ==========================================================================
// Speeds
// Timings
// ==========================================================================
$speeds: (
$timings: (
fastest: 0.1s,
faster: 0.15s,
fast: 0.25s,
normal: 0.3s,
slow: 0.5s,
slower: 0.75s,
slowest: 1s,
normal: 0.5s,
slow: 0.75s,
slower: 1s,
slowest: 2s,
);
// Default timing for t()
$timing-default: "normal" !default;
// Function
// ==========================================================================
@@ -22,17 +25,17 @@ $speeds: (
//
// ```scss
// .c-box {
// transition-duration: speed(slow);
// transition-duration: t(slow);
// }
// ```
//
// @param {string} $key - The speed key in $speeds.
// @param {string} $key - The timing key in $timings.
// @return {duration}
@function speed($key: "normal") {
@if not map-has-key($speeds, $key) {
@error "Unknown '#{$key}' in $speeds.";
@function t($key: $timing-default) {
@if not map-has-key($timings, $key) {
@error "Unknown '#{$key}' in $timings.";
}
@return map-get($speeds, $key);
@return map-get($timings, $key);
}

View File

@@ -12,7 +12,20 @@
// Container
--container-width: calc(100% - 2 * var(--grid-margin));
@media (min-width: $from-sm) {
// Font sizes
--font-size-h1: #{responsive-type(36px, 72px, 1400px)};
--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)};
// // Colors
// @each $color, $value in $colors {
// --color-#{"" + $color}: #{$value};
// }
@media (min-width: $from-small) {
--grid-columns: #{$base-column-nb};
--grid-gutter: #{rem(16px)};
--grid-margin: #{rem(20px)};

View File

@@ -48,6 +48,17 @@
@return math.div($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
@@ -114,11 +125,11 @@ $context: 'frontend' !default;
// }
// ```
//
// @param {number} $percentage - The percentage spacer
// @param {number} $inset - The grid gutter inset
// @param {number} $number - 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));
@return calc(#{$percentage} * (100vw - 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.
@@ -178,29 +189,16 @@ $context: 'frontend' !default;
@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);
// font-size: responsive-type(30px, 60px, 1800);
// }
//
// .c-heading.-h2 {
// font-size: responsive-value(20px, 40px, $from-xl);
// font-size: responsive-type(20px, 40px, $from-big);
// }
// ```
//
@@ -208,7 +206,7 @@ $context: 'frontend' !default;
// @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) {
@function responsive-type($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

@@ -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
////
@@ -34,7 +35,7 @@ $spacing-properties: (
'margin': 'margin',
) !default;
$spacing-sizes: join($spacings, (
$spacing-sizes: join($spacers, (
null: var(--grid-gutter),
'none': 0
));
@@ -50,8 +51,8 @@ $spacing-sizes: join($spacings, (
// Base class
$base-class: ".u-" + #{$property-namespace}#{$direction-namespace}#{$size-namespace};
// Spacing without media query
@if $breakpoint == "xs" {
// Spacer without media query
@if $breakpoint == "tiny" {
#{$base-class} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
@@ -59,7 +60,7 @@ $spacing-sizes: join($spacings, (
}
}
// Spacing min-width breakpoints `@from-*`
// Spacer min-width breakpoints `@from-*`
#{$base-class}\@from-#{$breakpoint} {
@media #{mq-min($breakpoint)} {
@each $direction in $directions {
@@ -68,7 +69,7 @@ $spacing-sizes: join($spacings, (
}
}
// Spacing max-width breakpoints @to-*`
// Spacer max-width breakpoints @to-*`
#{$base-class}\@to-#{$breakpoint} {
@media #{mq-max($breakpoint)} {
@each $direction in $directions {

View File

@@ -46,14 +46,14 @@
}
}
// .is-hidden\@to-lg {
// @media (max-width: $to-lg) {
// .is-hidden\@to-large {
// @media (max-width: $to-large) {
// display: none;
// }
// }
//
// .is-hidden\@from-lg {
// @media (min-width: $from-lg) {
// .is-hidden\@from-large {
// @media (min-width: $from-large) {
// display: none;
// }
// }

View File

@@ -21,8 +21,8 @@ $widths-fractions: 1 2 3 4 5 !default;
@include widths($widths-fractions);
.u-1\/2\@from-sm {
@media (min-width: $from-sm) {
.u-1\/2\@from-small {
@media (min-width: $from-small) {
width: 50%;
}
}

63
build/config/svgo.js Normal file
View File

@@ -0,0 +1,63 @@
/**
* @file Provides a custom configuration to optimize (but not minimize)
* individual SVG files.
*
* This configset will not work, as is, with spritesheets.
* You will need to remove the following plugins:
*
* - `cleanupIds`
* - `removeHiddenElems`
*/
export default {
multipass: true,
js2svg: {
indent: 4,
pretty: true,
},
plugins: [
'cleanupAttrs',
'cleanupEnableBackground',
'cleanupIds',
'cleanupListOfValues',
'cleanupNumericValues',
'collapseGroups',
'convertColors',
'convertEllipseToCircle',
'convertPathData',
'convertShapeToPath',
'convertStyleToAttrs',
'convertTransform',
'inlineStyles',
'mergePaths',
'mergeStyles',
'minifyStyles',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'removeComments',
'removeDesc',
'removeDimensions',
'removeDoctype',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeEmptyContainers',
'removeEmptyText',
'removeHiddenElems',
'removeMetadata',
'removeNonInheritableGroupAttrs',
'removeRasterImages',
'removeScriptElement',
'removeStyleElement',
'removeTitle',
'removeUnknownsAndDefaults',
'removeUnusedNS',
'removeUselessDefs',
'removeUselessStrokeAndFill',
'removeViewBox',
// 'removeXMLNS',
// 'removeXMLProcInst',
// 'reusePaths',
// 'sortAttrs',
'sortDefsChildren',
],
};

View File

@@ -2,14 +2,14 @@
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json' with { type: 'json' };
import loconfig from '../../loconfig.json' assert { type: 'json' };
import { merge } from '../utils/index.js';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json', {
with: { type: 'json' },
assert: { type: 'json' },
});
usrconfig = usrconfig.default;

26
build/helpers/svgo.js Normal file
View File

@@ -0,0 +1,26 @@
/**
* @file If available, returns the SVGO API.
*/
let createContentItem, extendDefaultPlugins, loadConfig, optimize;
try {
let svgo = await import('svgo');
({
createContentItem,
extendDefaultPlugins,
loadConfig,
optimize
} = svgo.default);
} catch (err) {
// do nothing
}
export default optimize;
export {
createContentItem,
extendDefaultPlugins,
loadConfig,
optimize
};

View File

@@ -10,9 +10,12 @@ import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { promisify } from 'node:util';
import * as sass from 'sass';
import { PurgeCSS } from 'purgecss';
const sassRender = promisify(sass.render);
let postcssProcessor;
/**
@@ -21,15 +24,16 @@ 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',
});
/**
@@ -123,7 +127,10 @@ 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 (typeof postcssProcessor === 'undefined') {

View File

@@ -1,59 +1,69 @@
import defaultSVGOOptions from '../config/svgo.js';
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 optimize from '../helpers/svgo.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import {
basename,
dirname,
extname,
resolve,
} from 'node:path';
import commonPath from 'common-path';
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);
/**
* @const {object} developmentSVGsArgs - The predefined `compileSVGs()` options for development.
* @const {object} productionSVGsArgs - The predefined `compileSVGs()` options for production.
* Exclude certain SVGO plugins for the purposes of building a spritesheet.
*/
const excludeSVGOPlugins = [
'cleanupIds',
'removeHiddenElems',
];
defaultSVGOOptions.plugins = defaultSVGOOptions.plugins.filter((plugin) => !excludeSVGOPlugins.includes(plugin));
defaultSVGOOptions.js2svg.pretty = false;
/**
* @const {object} defaultSVGOOptions - The default shared SVGO options.
* @const {object} developmentSVGOOptions - The predefined SVGO options for development.
* @const {object} productionSVGOOptions - The predefined SVGO options for production.
*/
export { defaultSVGOOptions };
export const developmentSVGOOptions = Object.assign({}, defaultSVGOOptions);
export const productionSVGOOptions = Object.assign({}, defaultSVGOOptions);
/**
* @const {object|boolean} developmentSVGsArgs - The predefined `compileSVGs()` options for development.
* @const {object|boolean} productionSVGsArgs - The predefined `compileSVGs()` options for production.
*/
export const developmentSVGsArgs = [
developmentMixerOptions,
false,
];
export const productionSVGsArgs = [
productionMixerOptions,
productionSVGOOptions,
];
/**
* Generates and transforms SVG spritesheets.
*
* @async
* @param {object} [mixerOptions=null] - Customize the Mixer API options.
* @param {object} [mixerOptions=null] - Customize the Mixer API options.
* If `null`, default production options are used.
* @param {object|boolean} [svgoOptions=null] - Customize the SVGO processor API options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function compileSVGs(mixerOptions = null) {
export default async function compileSVGs(mixerOptions = null, svgoOptions = null) {
if (mixerOptions == null) {
mixerOptions = productionMixerOptions;
} else if (
@@ -63,6 +73,18 @@ export default async function compileSVGs(mixerOptions = null) {
mixerOptions = merge({}, defaultMixerOptions, mixerOptions);
}
if (optimize) {
if (svgoOptions == null) {
svgoOptions = productionSVGOOptions;
} else if (
svgoOptions !== false &&
svgoOptions !== developmentSVGOOptions &&
svgoOptions !== productionSVGOOptions
) {
svgoOptions = Object.assign({}, defaultSVGOOptions, svgoOptions);
}
}
/**
* @async
* @param {object} entry - The entrypoint to process.
@@ -89,53 +111,15 @@ export default async function compileSVGs(mixerOptions = null) {
includes = [ includes ];
}
includes = resolveTemplate(includes);
outfile = resolveTemplate(outfile);
includes = resolve(includes);
outfile = resolve(outfile);
if (supportsGlob && basePath) {
includes = await glob(includes);
includes = [ ...new Set(includes) ];
const result = await mixer(includes, mixerOptions);
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)}`;
};
if (optimize && svgoOptions) {
result.content = optimize(result.content, svgoOptions).data;
}
const result = await mixer(includes, {
...mixerOptions,
});
await result.write(outfile);
message(`${label} compiled`, 'success', timeLabel);

View File

@@ -49,6 +49,9 @@ npm start
# Compile and minify assets
npm run build
# Optimize individual SVG files
npm run optimize:svg -- -f ./assets/images ./assets/images
```
See [`build.js`](../build/build.js) and [`watch.js`](../build/watch.js)
@@ -306,7 +309,7 @@ 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
A wrapper around [SVG Mixer] and [SVGO] for transforming and minifying SVG files
and generating spritesheets.
Example:
@@ -429,4 +432,5 @@ See [`versions.js`](../build/tasks/versions.js) for details.
[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
[SVGO]: https://npmjs.com/package/svgo
[tiny-glob]: https://npmjs.com/package/tiny-glob

View File

@@ -70,8 +70,8 @@ The first step is to set intial SCSS values in the following files :
grid-template-columns: repeat(4, 1fr);
}
&.-col-#{$base-column-nb}\@from-md {
@media (min-width: $from-md) {
&.-col-#{$base-column-nb}\@from-medium {
@media (min-width: $from-medium) {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
}

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);
}
}
}
}
```

View File

@@ -15,7 +15,7 @@
"dest": "./www/assets/scripts"
},
"svgs": {
"src": "./assets/svgs",
"src": "./assets/images/sprite",
"dest": "./www/assets/images"
},
"views": {

8903
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,42 +6,41 @@
"author": "Locomotive <info@locomotive.ca>",
"type": "module",
"engines": {
"node": ">=20",
"npm": ">=10"
"node": ">=20.1",
"npm": ">=8.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",
"optimize:svg": "svgo --config=build/config/svgo.js"
},
"dependencies": {
"locomotive-scroll": "^5.0.0-beta.21",
"locomotive-scroll": "^5.0.0-beta.11",
"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",
"autoprefixer": "^10.4.17",
"browser-sync": "^3.0.2",
"common-path": "^1.0.1",
"concat": "^1.0.3",
"esbuild": "^0.24.2",
"esbuild": "^0.20.0",
"kleur": "^4.1.5",
"node-notifier": "^10.0.1",
"postcss": "^8.4.49",
"purgecss": "^7.0.2",
"sass": "^1.79.5",
"svg-mixer": "^2.4.0",
"postcss": "^8.4.21",
"purgecss": "^5.0.0",
"sass": "^1.70.0",
"svg-mixer": "~2.3.14",
"svgo": "^3.2.0",
"tiny-glob": "^0.2.9"
},
"overrides": {
"browser-sync": {
"ua-parser-js": "^1.0.33"
"ua-parser-js": "~1.0.33"
},
"svg-mixer": {
"micromatch": "^4.0.8",
"postcss": "^8.4.49"
},
"svg-mixer-utils": {
"anymatch": "^3.1.3"
"postcss": "^8.4.20"
}
}
}

View File

@@ -1 +1 @@
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"/>

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 59 B

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"critical.css"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -96,6 +96,9 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js" crossorigin="anonymous"></script>
<script nomodule src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.remove%2CElement.prototype.append%2Cfetch%2CCustomEvent%2CElement.prototype.matches%2CNodeList.prototype.forEach%2CAbortController" crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>

View File

@@ -83,6 +83,12 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"
crossorigin="anonymous"></script>
<script nomodule
src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.remove%2CElement.prototype.append%2Cfetch%2CCustomEvent%2CElement.prototype.matches%2CNodeList.prototype.forEach%2CAbortController"
crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>

View File

@@ -122,6 +122,9 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js" crossorigin="anonymous"></script>
<script nomodule src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.remove%2CElement.prototype.append%2Cfetch%2CCustomEvent%2CElement.prototype.matches%2CNodeList.prototype.forEach%2CAbortController" crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>

View File

@@ -69,6 +69,12 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"
crossorigin="anonymous"></script>
<script nomodule
src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.remove%2CElement.prototype.append%2Cfetch%2CCustomEvent%2CElement.prototype.matches%2CNodeList.prototype.forEach%2CAbortController"
crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>