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

96 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
Chauncey McAskill
962ba66b96 Update NPM dependencies
Updated:
- autoprefixer v10.4.13 → v10.4.17
- esbuild v0.17.6 → →0.20.0
- locomotive-scroll v5.0.0-beta.9 → v5.0.0-beta.11
- sass v1.69.5 → v1.70.0
2024-01-29 12:30:58 -05:00
Chauncey McAskill
5b6bca6ce3 Replace deprecated division in '_widths.scss' 2024-01-29 11:37:39 -05:00
Chauncey McAskill
4ae90a5821 Annotate Sass modules section of 'main.scss' 2024-01-29 11:37:18 -05:00
Lucas
31061daf60 Merge pull request #166 from locomotivemtl/feature/viewport-units
Viewport sizes fallbacks
2024-01-22 16:31:21 +01:00
Lucas
ceefeb554e Merge branch 'master' into feature/viewport-units 2024-01-22 16:31:12 +01:00
Lucas Vallenet
276b5eebc0 Update asset version 2024-01-22 16:30:40 +01:00
Jérémy Minié
a37c5b047a Fix PurgeCSS safelist regex 2024-01-16 15:07:09 -05:00
Lucas Vallenet
3f7077b488 Add vh/svh/dvh/lvh fallbacks 2024-01-11 11:45:46 +01:00
Deven Caron
61b6222525 Merge pull request #163 from locomotivemtl/chore/update-node-version
Update node version & bump npm packages version
2024-01-05 15:24:22 -05:00
Deven Caron
5acd27d1b0 Compile assets 2024-01-05 09:06:33 -05:00
Deven Caron
9a5a91b221 Update Node version and npm dependencies 2024-01-05 09:06:24 -05:00
Lucas
43c86c3b50 Merge pull request #160 from butterfail/greg/invlerp
Fix invlerp method
2023-12-11 11:10:26 +01:00
Lucas Vallenet
0b4c82ceda Add scss-config dedicated files 2023-12-11 11:02:54 +01:00
Lucas Vallenet
b72fdabe23 Add default z-index value 2023-12-11 11:00:21 +01:00
Lucas Vallenet
f1ebc27a69 Add aspect-ratio scss mixin 2023-12-11 10:58:24 +01:00
Grégoire Ciles
f2898c8c8e Fix invlerp method 2023-11-29 12:27:01 +01:00
Lucas Vallenet
12d65db09f Reverse aspect-ratio condition 2023-11-29 11:10:32 +01:00
Lucas Vallenet
657fd41f70 Update config functions: alpha parameter for colors, multiplier parameter for spacers 2023-11-21 10:03:17 +01:00
Deven Caron
65c486b910 Update locomotive-scroll to v5.0.0-beta.9 2023-11-15 13:55:25 -05:00
Deven Caron
3d0e4d26a2 Update sass version to 1.69.5 2023-11-15 13:53:04 -05:00
Chauncey McAskill
df1a3a6f3c Update sass import in styles.js 2023-09-08 17:11:27 -04:00
Lucas Vallenet
7f1b6dad2e Fix device detection on config.js 2023-09-05 09:40:19 +02:00
Lucas Vallenet
63e46cde26 Comment updates 2023-09-05 09:34:24 +02:00
Lucas Vallenet
6564fb330a Update eases names to match gsap / Add default vars for z-indexes, timings and eases 2023-09-05 09:31:56 +02:00
Chauncey McAskill
b5753148f1 Improve asset versioning task
Add support for "increment" format to increment an integer version number with support for "increment:semver" to increment the build or patch of a SemVer version number.

Usage:

```json
"versions": [
    {
        "format": "increment",
        "key": "version",
        "outfile": "assets.json"
    }
]
```

```jsonc
{
    "version": 16 // → 17
}
```

```json
"versions": [
    {
        "format": "increment:semver",
        "key": "version",
        "outfile": "assets.json"
    }
]
```

```jsonc
{
    "version": "1.0.0" // → 1.0.1
}
```

```jsonc
{
    "version": "1.0.0+1" // → 1.0.0+2
}
```
2023-08-14 16:56:01 -04:00
Lucas Vallenet
7b415af8c2 Fix comments 2023-08-14 10:56:14 +02:00
Lucas Vallenet
dc0bc2042c Create aspect-ratio SCSS mixin polyfill 2023-08-14 10:40:24 +02:00
Chauncey McAskill
e9dbb03207 Fix support for undefined tasks in watch.js
Amends 596ff7a8ee
2023-08-10 11:01:50 -04:00
Chauncey McAskill
95caf9ebb5 Replace window.onload with window.addEventListener()
Since the `window` can only ever have one `onload` value, if ever a third-party script overwrites this property, the application risks never initializing.

By using `addEventListener('load',…)`, the application's bootstrapping logic is queued and is more likely to be initialized.
2023-08-08 15:34:01 -04:00
Chauncey McAskill
596ff7a8ee Fix support for undefined tasks
For example, if there are no `concats` tasks defined in 'loconfig.json', avoid throwing an error.
2023-08-08 14:49:47 -04:00
Deven Caron
70b36052e6 Merge pull request #154 from locomotivemtl/dependabot/npm_and_yarn/socket.io-parser-4.2.4
Bump socket.io-parser from 4.2.2 to 4.2.4
2023-07-31 10:56:07 -04:00
dependabot[bot]
f1e2e2270f Bump socket.io-parser from 4.2.2 to 4.2.4
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.4)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 14:30:51 +00:00
Deven Caron
13735c64f9 Merge pull request #153 from locomotivemtl/feature/update-scroll
Update locomotive scroll v5
2023-07-31 10:30:17 -04:00
Deven Caron
659ef3767b Fix img scroll lazyload $el 2023-07-31 10:29:02 -04:00
Deven Caron
e162af17dd Update LS styles vendor import 2023-07-31 10:28:35 -04:00
Lucas Vallenet
93559a0c84 Add purgeCSS for .u-padding and .u-margin classes | Disable purge on development 2023-07-10 15:40:56 +02:00
Lucas Vallenet
c1bcf7fb0d Fix spacing loop 2023-06-16 10:13:10 +02:00
Lucas Vallenet
d0fcfaac86 Add config.spacers and modify spacing function to add breakpoint based spacers. 2023-06-15 17:28:29 +02:00
Lucas Vallenet
4a958c5fb5 Update grid-column loop with config.breakpoints list and media query functions. Add @to-* modifier 2023-06-15 17:28:06 +02:00
Lucas Vallenet
5e7e92c7f5 Rename z-indexes config / Update easing comment / Move font-face include to document.scss 2023-06-12 16:50:40 +02:00
Lucas Vallenet
380fbd40c3 Update comments 2023-06-12 16:21:57 +02:00
Lucas Vallenet
e16ba2ca16 Import locomotive scroll CSS / Removed unused styles (scroll, scorllbar) / Update DOM Markup / Update doc 2023-06-12 09:36:14 +02:00
Lucas Vallenet
43a5eb1ad3 Update locomotive scroll v5 2023-06-08 15:44:35 +02:00
Lucas Vallenet
99801a2d8b Add dedicated scss config files 2023-06-08 11:48:00 +02:00
Lucas Vallenet
217a1adba7 Merge scss functions / Fix strip-unit 2023-06-08 10:57:59 +02:00
Lucas Vallenet
a11e98e31e Fix js FONT var error 2023-05-22 09:48:34 +02:00
Deven Caron
6ef90dbe11 Merge pull request #149 from locomotivemtl/dependabot/npm_and_yarn/engine.io-6.4.2
Bump engine.io from 6.4.0 to 6.4.2
2023-05-12 10:41:24 -04:00
Deven Caron
6726d665f2 Merge pull request #148 from locomotivemtl/feature/scss-colors
Color as list
2023-05-12 10:39:38 -04:00
Deven Caron
dca6c5de1d Merge branch 'master' into feature/scss-colors 2023-05-12 10:39:10 -04:00
Deven Caron
05a00c4258 Merge pull request #146 from locomotivemtl/feature/css-math
Replace fractions with math.div in scss files
2023-05-12 10:36:33 -04:00
Deven Caron
7517be0e76 Merge pull request #147 from locomotivemtl/feature/js-config
Feature/js config
2023-05-12 10:36:08 -04:00
dependabot[bot]
dcec21adf4 Bump engine.io from 6.4.0 to 6.4.2
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.4.0 to 6.4.2.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.4.0...6.4.2)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-04 01:31:42 +00:00
Lucas Vallenet
297e0b4ec8 Update color function 2023-04-21 11:34:15 +02:00
Lucas Vallenet
9a2083d894 Color as list with function 2023-04-19 15:37:52 +02:00
Lucas Vallenet
1a81c865ae Config namespaces 2023-04-05 15:06:51 -04:00
Lucas Vallenet
d6b5784cdd Replace fractions with math.div in scss files 2023-04-05 14:40:28 +02:00
Lucas Vallenet
8894664743 Update js config values and use it within whole app 2023-04-05 12:23:25 +02:00
Deven Caron
8aac2ffea6 Merge pull request #132 from locomotivemtl/feature/build-task-chores
Various fixes and changes to build utilities
2023-03-06 09:07:04 -05:00
Chauncey McAskill
349d110dee Revert merging of usrconfig to use let declaration 2023-03-03 13:13:31 -05:00
Chauncey McAskill
7be5e48f22 Fix import of merge util in 'watch.js'
The `merge` function is provided by 'utils/index.js'.
2023-03-03 13:07:47 -05:00
Chauncey McAskill
1ee315663e Fix glob.js and improve documentation
Documented API inconsistencies between supported glob libraries (expected/supported parameters and return types).

Fixed API inconsistencies between 'tiny-glob', 'globby', and 'glob', to match 'fast-glob'.

Fixed broken support for preset options for 'tiny-glob'.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
89bb00790f Update postcss.js documentation 2023-03-03 13:04:22 -05:00
Chauncey McAskill
9db0c71a82 Move helpers/utils exports to end of file
Improves readability by always expecting exports at the end of the file.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
7742bbb9d0 Add block comments to task iteratees 2023-03-03 13:04:22 -05:00
Chauncey McAskill
c9a9209b4b Refactor build utilities
Separated generic functions from build helpers.

Changed:
- Moved 'utils/*.js' to 'helpers/*.js'
- From 'utils/config.js':
  - Moved function `merge` to 'utils/index.js'.
  - Moved function `isObjectLike` to 'utils/index.js'.
- From 'utils/template.js':
  - Moved function `flatten` to 'utils/index.js'.
  - Moved function `escapeRegExp` to 'utils/index.js'.
- From 'tasks/styles.js':
  - Moved function `createPostCSSProcessor` to 'helpers/postcss.js' as `createProcessor`.
- Replaced function `Object.assign` with `merge` for task options parsing in all tasks.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
9d758f3b2c Improve postcss.js
Added constant `supportsPostCSS` to provide a boolean to check if PostCSS is available.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
0738dd6491 Improve glob.js
Added constant `supportsGlob` to provide a boolean to check if a glob function is available.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
a4656f59ed Improve concats.js, scripts.js, svgs.js
Added:
- Condition to cast `includes` into an array.

Removed:
- Variable `files` in favour of reusing `includes` in 'concats.js'.
2023-03-03 13:04:20 -05:00
Deven Caron
7ff6094e40 Move sass to dev dependencies 2023-03-03 10:49:57 -05:00
Chauncey McAskill
3fee6f4888 Update NPM depdencies
Updated:
- browser-sync v2.27.5 → v2.27.11
- esbuild v0.16.17 → v0.17.6
- engine.io v3.5.0 → v6.4.0
- qs v6.2.3 → v6.11.0

Changed:
- Overrode ua-parser-js constraint in browser-sync from `1.0.2` to `~1.0.33` to fix security notice.
2023-02-09 10:52:26 -05:00
Deven Caron
f774482255 Update README node version 2023-02-09 10:21:39 -05:00
Deven Caron
b7d9311ac6 Remove container width; replace with padding 2023-02-09 10:18:47 -05:00
Deven Caron
c22a006079 Merge pull request #139 from locomotivemtl/feature/update-node-version
Update node version to v17.9 and switch from node-sass to sass
2023-02-09 10:15:42 -05:00
Deven Caron
af57ebd9cb Merge branch 'master' into feature/update-node-version 2023-02-09 10:11:24 -05:00
Deven Caron
c82e9916d0 Merge branch 'master' into feature/update-node-version 2023-02-09 10:05:25 -05:00
Deven Caron
943324220a Downgrade to node v17.9 2023-02-09 10:00:31 -05:00
Deven Caron
477cec7763 Merge pull request #141 from locomotivemtl/feature/optimize-grid
Optimise grid-column loops / Use css vars for grid columns
2023-02-09 09:08:35 -05:00
Deven Caron
5d38685460 Merge branch 'master' into feature/optimize-grid 2023-02-09 09:07:35 -05:00
Deven Caron
9079d735bc Merge pull request #138 from locomotivemtl/feature/css-variables
Feature / CSS Variables
2023-02-09 09:02:59 -05:00
Deven Caron
87238fcdd5 Merge branch 'master' into feature/css-variables 2023-02-09 09:01:24 -05:00
Deven Caron
2f75d8f3d2 Merge pull request #142 from locomotivemtl/dependabot/npm_and_yarn/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1
2023-02-07 09:30:26 -05:00
dependabot[bot]
0346a15b57 Bump http-cache-semantics from 4.1.0 to 4.1.1
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-04 05:43:43 +00:00
Deven Caron
a2d658bc13 Remove log to prevent throwing unwanted errors 2023-02-02 10:14:48 -05:00
Deven Caron
7c1b61eda9 Add build config dynamic import assertation type 2023-02-02 10:00:20 -05:00
Lucas Vallenet
1fe30a9837 Optimize grid-column loops / Use css vars for grid columns 2023-02-01 10:23:43 +01:00
Deven Caron
e4ae03a94c Remove unused sass env function 2023-01-31 15:37:40 -05:00
Deven Caron
3cde7d40ee Update readme 2023-01-31 14:15:56 -05:00
Deven Caron
d1d4fb5fe5 Bump node-version & upgrade node-sass to sass 2023-01-31 13:57:51 -05:00
Lucas Vallenet
be71474633 Remove duplicate / Update functions descriptions 2023-01-05 09:58:01 +01:00
Lucas Vallenet
810df92a61 Fix functions.scss error 2023-01-04 14:41:25 +01:00
Lucas Vallenet
aba77ea2d9 Add default CSS Variables 2023-01-04 11:21:43 +01:00
Lucas Vallenet
bf28fe21a3 Strip breakpoint unit from responsive-type 2023-01-04 11:02:32 +01:00
Lucas Vallenet
7b3cefd8df Add usefull scss functions 2023-01-04 10:50:36 +01:00
71 changed files with 3334 additions and 6115 deletions

2
.nvmrc
View File

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

View File

@@ -23,7 +23,7 @@ Learn more about [languages and technologies](docs/technologies.md).
Make sure you have the following installed:
* [Node] — at least 14.17, 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.

View File

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

View File

@@ -1,59 +1,110 @@
import modular from 'modujs';
import * as modules from './modules';
import globals from './globals';
import { html } from './utils/environment';
import config from './config'
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';
const app = new modular({
modules: modules,
modules,
});
window.onload = (event) => {
const 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 height = window.innerHeight;
const svh = document.documentElement.clientHeight * 0.01;
documentStyles.setProperty('--svh', `${svh}px`);
const dvh = height * 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`);
}
}
window.addEventListener('load', () => {
const $style = document.getElementById('main-css');
if ($style) {
if ($style.isLoaded) {
init();
} else {
$style.addEventListener('load', (event) => {
init();
});
$style.addEventListener('load', init);
}
} else {
console.warn('The "main-css" stylesheet not found');
}
};
export const EAGER_FONTS = [
{ family: 'Source Sans', style: 'normal', weight: 400 },
{ family: 'Source Sans', style: 'normal', weight: 700 },
];
});
function init() {
globals();
setViewportSizes();
app.init(app);
html.classList.add('is-loaded');
html.classList.add('is-ready');
html.classList.remove('is-loading');
$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(EAGER_FONTS, config.IS_DEV).then((eagerFonts) => {
html.classList.add('fonts-loaded');
loadFonts(FONT.EAGER, ENV.IS_DEV).then((eagerFonts) => {
$html.classList.add(CSS_CLASS.FONTS_LOADED);
if (config.IS_DEV) {
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/*, font*/))
console.groupEnd()
console.group('State of all fonts:')
document.fonts.forEach((font) => console.log(font.family, font.style, font.weight, font.status/*, font*/))
console.groupEnd()
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

@@ -7,18 +7,59 @@
* > (since `process` is a Node API, not a web API).
* > — https://esbuild.github.io/api/#platform
*/
const env = process.env.NODE_ENV
export default config = Object.freeze({
// Environments
ENV: env,
IS_PROD: env === 'production',
IS_DEV: env === 'development',
const NODE_ENV = process.env.NODE_ENV
const IS_MOBILE = window.matchMedia('(any-pointer:coarse)').matches
// CSS class names
CSS_CLASS: {
LOADING: 'is-loading',
READY: 'is-ready',
LOADED: 'is-loaded',
},
// 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,10 +1,10 @@
import svg4everybody from 'svg4everybody';
import config from './config';
import { ENV } from './config';
// Dynamic imports for development mode only
let gridHelper;
(async () => {
if (config.IS_DEV) {
if (ENV.IS_DEV) {
const gridHelperModule = await import('./utils/grid-helper');
gridHelper = gridHelperModule?.gridHelper;
}

View File

@@ -1,5 +1,5 @@
import { module } from 'modujs';
import { EAGER_FONTS } from '../app';
import { FONT } from '../config';
import { whenReady } from '../utils/fonts';
export default class extends module {
@@ -8,7 +8,7 @@ export default class extends module {
}
init() {
whenReady(EAGER_FONTS).then((fonts) => this.onFontsLoaded(fonts));
whenReady(FONT.EAGER).then((fonts) => this.onFontsLoaded(fonts));
}
onFontsLoaded(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,18 +9,14 @@ export default class extends module {
init() {
this.scroll = new LocomotiveScroll({
el: this.el,
smooth: true
});
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);
modularInstance: this,
})
// // Force scroll to top
// if (history.scrollRestoration) {
// history.scrollRestoration = 'manual'
// window.scrollTo(0, 0)
// }
}
/**
@@ -41,11 +37,22 @@ export default class extends module {
* @param {LocomotiveScroll} args - The Locomotive Scroll instance.
*/
lazyLoad(args) {
lazyLoadImage(args.obj.el, null, () => {
lazyLoadImage(args.target, 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,7 @@
const $html = document.documentElement
const $body = document.body
export {
$html,
$body,
}

View File

@@ -1,9 +0,0 @@
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,3 +1,5 @@
import { CSS_CLASS } from '../config'
/**
* Get an image meta data
*
@@ -89,14 +91,14 @@ const lazyLoadImage = async ($el, url, callback) => {
}
requestAnimationFrame(() => {
let lazyParent = $el.closest('.c-lazy')
let lazyParent = $el.closest(`.${CSS_CLASS.LAZY_CONTAINER}`)
if(lazyParent) {
lazyParent.classList.add('-lazy-loaded')
lazyParent.classList.add(CSS_CLASS.LAZY_LOADED)
lazyParent.style.backgroundImage = ''
}
$el.classList.add('-lazy-loaded')
$el.classList.add(CSS_CLASS.LAZY_LOADED)
callback?.()
})

View File

@@ -28,7 +28,7 @@ const lerp = (x, y, a) => x * (1 - a) + y * a
* @return {number} inverted lerp value
*/
const invlerp = (x, y, a) => clamp((v - x)/(a - x))
const invlerp = (x, y, a) => clamp((a - x)/(y - x))
/**

View File

@@ -27,7 +27,7 @@ $input-icon-color: 424242; // No #
.c-form_input {
padding: rem(10px);
border: 1px solid lightgray;
background-color: $color-lightest;
background-color: color(lightest);
&:hover {
border-color: darkgray;
@@ -63,7 +63,7 @@ $checkbox-icon-color: $input-icon-color;
top: 50%;
left: 0;
display: inline-block;
margin-top: (-$checkbox / 2);
margin-top: math.div(-$checkbox, 2);
padding: 0;
width: $checkbox;
height: $checkbox;
@@ -71,7 +71,7 @@ $checkbox-icon-color: $input-icon-color;
}
&::before {
background-color: $color-lightest;
background-color: color(lightest);
border: 1px solid lightgray;
}

View File

@@ -3,30 +3,29 @@
// ==========================================================================
.c-heading {
line-height: $line-height-h;
margin-bottom: rem(30px);
&.-h1 {
font-size: rem($font-size-h1);
font-size: var(--font-size-h1);
}
&.-h2 {
font-size: rem($font-size-h2);
font-size: var(--font-size-h2);
}
&.-h3 {
font-size: rem($font-size-h3);
font-size: var(--font-size-h3);
}
&.-h4 {
font-size: rem($font-size-h4);
font-size: var(--font-size-h4);
}
&.-h5 {
font-size: rem($font-size-h5);
font-size: var(--font-size-h5);
}
&.-h6 {
font-size: rem($font-size-h6);
font-size: var(--font-size-h6);
}
}

View File

@@ -9,7 +9,7 @@
width: 11px;
height: 100vh;
transform-origin: center right;
transition: transform 0.3s, opacity 0.3s;
transition: transform t(normal), opacity t(normal);
opacity: 0;
&:hover {
@@ -25,7 +25,7 @@
position: absolute;
top: 0;
right: 0;
background-color: $color-darkest;
background-color: color(darkest);
opacity: 0.5;
width: 7px;
border-radius: 10px;

View File

@@ -2,25 +2,10 @@
// Elements / Document
// ==========================================================================
:root {
// Grid
--grid-columns : 4;
--grid-gutter : #{rem(10px)};
--grid-gutter-half : calc(0.5 * var(--grid-gutter));
--grid-margin : 0px;
@media (min-width: $from-small) {
--grid-columns : 12;
--grid-gutter : #{rem(16px)};
--grid-margin : #{rem(20px)};
}
}
//
// Simple page-level setup.
//
// 1. Include web fonts
// 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.
@@ -66,30 +51,14 @@ html {
&.is-loading {
cursor: wait;
}
&.has-scroll-smooth {
overflow: hidden;
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
}
&.has-scroll-dragging {
user-select: none;
}
}
body {
.has-scroll-smooth & {
overflow: hidden;
}
}
::selection {
background-color: $selection-background-color;
color: $selection-text-color;
background-color: $color-selection-background;
color: $color-selection-text;
text-shadow: none;
}

View File

@@ -1,27 +1,35 @@
// ==========================================================================
// Main
// ==========================================================================
$app-env: app-env();
// Settings
// Modules
// ==========================================================================
@import "settings/config.eases";
@import "settings/config.colors";
@import "settings/config";
@use "sass:math";
// ==========================================================================
// Tools
// ==========================================================================
@import "tools/maths";
@import "tools/functions";
@import "tools/mixins";
@import "tools/fonts";
// @import "tools/layout";
// @import "tools/widths";
// @import "tools/family";
// Settings
// ==========================================================================
@import "settings/config.breakpoints";
@import "settings/config.colors";
@import "settings/config.eases";
@import "settings/config.fonts";
@import "settings/config.spacers";
@import "settings/config.timings";
@import "settings/config.zindexes";
@import "settings/config";
@import "settings/config.variables";
// Generic
// ==========================================================================
@@ -31,6 +39,10 @@ $app-env: app-env();
@import "generic/form";
@import "generic/button";
// Vendors
// ==========================================================================
@import "node_modules/locomotive-scroll/dist/locomotive-scroll";
// Elements
// ==========================================================================
@@ -39,23 +51,16 @@ $app-env: app-env();
// Objects
// ==========================================================================
@import "objects/scroll";
@import "objects/container";
@import "objects/ratio";
@import "objects/icons";
@import "objects/grid";
// @import "objects/layout";
// @import "objects/crop";
// @import "objects/table";
// Vendors
// ==========================================================================
// @import "vendors/vendor";
// Components
// ==========================================================================
@import "components/scrollbar";
@import "components/heading";
@import "components/button";
@import "components/form";
@@ -69,5 +74,5 @@ $app-env: app-env();
// @import "utilities/align";
// @import "utilities/helpers";
// @import "utilities/states";
// @import "utilities/spacing";
// @import "utilities/print";
@import "utilities/spacing";
// @import "utilities/print";

View File

@@ -13,6 +13,6 @@
.o-container {
margin-right: auto;
margin-left: auto;
padding-right: $base-column-gap;
padding-left: $base-column-gap;
padding-left: var(--grid-margin);
padding-right: var(--grid-margin);
}

View File

@@ -1,83 +0,0 @@
// ==========================================================================
// 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

@@ -31,6 +31,12 @@
// ==========================================================================
// 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);
}
@@ -51,8 +57,8 @@
// Gutters rows and columns
&.-gutters {
gap: $base-column-gap;
column-gap: $base-column-gap;
gap: var(--grid-gutter);
column-gap: var(--grid-gutter);
}
// ==========================================================================
@@ -163,7 +169,8 @@
// By default, a grid item takes full width of its parent.
//
.o-grid_item {
grid-column: 1 / -1;
grid-column-start: var(--gc-start, 1);
grid-column-end: var(--gc-end, -1);
&.-align-end {
align-self: end;

View File

@@ -32,7 +32,7 @@
vertical-align: middle;
svg {
--icon-height: calc(var(--icon-width) * (1 / (var(--icon-ratio))));
--icon-height: calc(var(--icon-width) * math.div(1, (var(--icon-ratio))));
display: block;
width: var(--icon-width);

View File

@@ -1,7 +0,0 @@
// ==========================================================================
// Objects / Scroll
// ==========================================================================
.o-scroll {
min-height: 100vh;
}

View File

@@ -0,0 +1,95 @@
// ==========================================================================
// Settings / Config / Breakpoints
// ==========================================================================
// Breakpoints
// ==========================================================================
$breakpoints: (
"tiny": 500px,
"small": 700px,
"medium": 1000px,
"large": 1200px,
"big": 1400px,
"huge": 1600px,
"enormous": 1800px,
"gigantic": 2000px,
"colossal": 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-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

@@ -3,26 +3,56 @@
// ==========================================================================
// Palette
// =============================================================================
// ==========================================================================
$color-lightest: #FFFFFF;
$color-darkest: #000000;
$colors: (
primary: #3297FD,
lightest: #FFFFFF,
darkest: #000000,
);
// Specific
// =============================================================================
// Function
// ==========================================================================
// Returns color code.
//
// ```scss
// .c-box {
// color: color(primary);
// }
// ```
//
// @param {string} $key - The color key in $colors.
// @param {number} $alpha - The alpha for the color value.
// @return {color}
@function color($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
// ==========================================================================
// Link
$color-link: #1A0DAB;
$color-link-focus: #1A0DAB;
$color-link-hover: darken(#1A0DAB, 10%);
$color-link: color(primary);
$color-link-focus: color(primary);
$color-link-hover: darken(color(primary), 10%);
// Selection
$selection-text-color: #3297FD;
$selection-background-color: #FFFFFF;
// Social Colors
// =============================================================================
$color-selection-text: color(darkest);
$color-selection-background: color(lightest);
// Socials
$color-facebook: #3B5998;
$color-instagram: #E1306C;
$color-youtube: #CD201F;

View File

@@ -2,47 +2,77 @@
// Settings / Config / Eases
// ==========================================================================
// Power 1
$ease-power1-in: cubic-bezier(0.550, 0.085, 0.680, 0.530);
$ease-power1-out: cubic-bezier(0.250, 0.460, 0.450, 0.940);
$ease-power1-in-out: cubic-bezier(0.455, 0.030, 0.515, 0.955);
// Eases
// ==========================================================================
// Power 2
$ease-power2-in: cubic-bezier(0.550, 0.055, 0.675, 0.190);
$ease-power2-out: cubic-bezier(0.215, 0.610, 0.355, 1.000);
$ease-power2-in-out: cubic-bezier(0.645, 0.045, 0.355, 1.000);
$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 3
$ease-power3-in: cubic-bezier(0.895, 0.030, 0.685, 0.220);
$ease-power3-out: cubic-bezier(0.165, 0.840, 0.440, 1.000);
$ease-power3-in-out: cubic-bezier(0.770, 0.000, 0.175, 1.000);
// 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
$ease-power4-in: cubic-bezier(0.755, 0.050, 0.855, 0.060);
$ease-power4-out: cubic-bezier(0.230, 1.000, 0.320, 1.000);
$ease-power4-in-out: cubic-bezier(0.860, 0.000, 0.070, 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),
// Expo
$ease-expo-in: cubic-bezier(0.950, 0.050, 0.795, 0.035);
$ease-expo-out: cubic-bezier(0.190, 1.000, 0.220, 1.000);
$ease-expo-in-out: cubic-bezier(1.000, 0.000, 0.000, 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),
// Back
$ease-back-in: cubic-bezier(0.600, -0.280, 0.735, 0.045);
$ease-back-out: cubic-bezier(0.175, 00.885, 0.320, 1.275);
$ease-back-in-out: cubic-bezier(0.680, -0.550, 0.265, 1.550);
// 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),
// Sine
$ease-sine-in: cubic-bezier(0.470, 0.000, 0.745, 0.715);
$ease-sine-out: cubic-bezier(0.390, 0.575, 0.565, 1.000);
$ease-sine-in-out: cubic-bezier(0.445, 0.050, 0.550, 0.950);
// 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),
// Circ
$ease-circ-in: cubic-bezier(0.600, 0.040, 0.980, 0.335);
$ease-circ-out: cubic-bezier(0.075, 0.820, 0.165, 1.000);
$ease-circ-in-out: cubic-bezier(0.785, 0.135, 0.150, 0.860);
// 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),
// Misc
$ease-bounce: cubic-bezier(0.17, 0.67, 0.3, 1.33);
$ease-slow-out: cubic-bezier(.04,1.15,0.4,.99);
$ease-smooth: cubic-bezier(0.380, 0.005, 0.215, 1);
// 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);
}

View File

@@ -1,5 +1,42 @@
// ==========================================================================
// Tools / Font Faces
// 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.

View File

@@ -11,54 +11,13 @@ $context: frontend !default;
// Path is relative to the stylesheets directory.
$assets-path: "../" !default;
// Typefaces
// =============================================================================
// Font directory
$font-dir: "../fonts/";
// 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;
// Map of font families.
//
// ```
// <font-id>: (<font-name>, <font-fallbacks>)
// ```
$font-families: (
sans: join("Source Sans", $font-fallback-sans, $separator: comma),
);
// 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),
);
// Typography
// =============================================================================
// Base
$font-size: 16px;
$line-height: 24px / $font-size;
$font-color: $color-darkest;
// Headings
$font-size-h1: 36px !default;
$font-size-h2: 28px !default;
$font-size-h3: 24px !default;
$font-size-h4: 20px !default;
$font-size-h5: 18px !default;
$font-size-h6: 16px !default;
$line-height-h: $line-height;
$font-size: 16px;
$line-height: math.div(24px, $font-size);
$font-color: color(darkest);
// Weights
$font-weight-light: 300;
@@ -66,11 +25,10 @@ $font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-bold: 700;
// Transitions
// Transition defaults
// =============================================================================
$speed: 0.3s;
$easing: $ease-power2-out;
$speed: t(normal);
$easing: ease("power2.out");
// Spacing Units
// =============================================================================
@@ -84,35 +42,3 @@ $padding: $unit;
// Grid
// ==========================================================================
$base-column-nb: 12;
$base-column-gap: $unit-small;
// 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;
// Master z-indexe
// =============================================================================
$z-indexes: (
"header": 200,
"above": 1,
"below": -1
);

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

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

View File

@@ -0,0 +1,33 @@
// ==========================================================================
// 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));
// 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

@@ -0,0 +1,44 @@
// ==========================================================================
// 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

@@ -147,7 +147,7 @@
// @param {number} $num - id of the child
@mixin middle($num) {
&:nth-child(#{round($num / 2)}) {
&:nth-child(#{round(math.div($num, 2))}) {
@content;
}
}

View File

@@ -26,7 +26,7 @@
@error "`#{$base}` needs to be a number in pixel.";
}
@return ($size / $base) * 1em;
@return math.div($size, $base) * 1em;
}
// Converts the given pixel value to its REM quivalent.
@@ -45,32 +45,7 @@
@error "`#{$base}` needs to be a number in pixel.";
}
@return ($size / $base) * 1rem;
}
// 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, $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;
@return math.div($size, $base) * 1rem;
}
// Converts a number to a percentage.
@@ -139,3 +114,99 @@
}
$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} $number - The percentage spacer
// @param {number} $inset - The grid gutter inset
// @return {function<number>}
@function grid-space($percentage, $inset: 0) {
@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.
//
// ```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));
}
// Returns clamp of calculated preferred responsive font size
// within a font size and breakpoint range.
//
// ```scss
// .c-heading.-h1 {
// font-size: responsive-type(30px, 60px, 1800);
// }
//
// .c-heading.-h2 {
// font-size: responsive-type(20px, 40px, $from-big);
// }
// ```
//
// @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-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

@@ -2,13 +2,18 @@
// Tools / Maths
// ==========================================================================
// Removes the unit from the given number.
// Remove the unit of a length
//
// @param {number} $number The number to strip.
// @return {number}
// @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);
}
@function strip-units($number) {
@return $number / ($number * 0 + 1);
@return $value;
}
// Returns the square root of the given number.
@@ -21,7 +26,7 @@
$value: $x;
@for $i from 1 through 10 {
$value: $x - ($x * $x - abs($number)) / (2 * $x);
$value: $x - math.div(($x * $x - abs($number)), (2 * $x));
$x: $value;
}
@@ -43,7 +48,7 @@
}
} @else if $exp < 0 {
@for $i from 1 through -$exp {
$value: $value / $number;
$value: math.div($value, $number);
}
}
@@ -88,7 +93,7 @@
// If the angle has `deg` as unit, convert to radians.
@if ($unit == deg) {
@return $angle / 180 * pi();
@return math.div($angle, 180) * pi();
}
@return $angle;
@@ -104,7 +109,7 @@
$angle: rad($angle);
@for $i from 0 through 10 {
$sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1);
$sin: $sin + pow(-1, $i) * math.div(pow($angle, (2 * $i + 1)), fact(2 * $i + 1));
}
@return $sin;
@@ -120,7 +125,7 @@
$angle: rad($angle);
@for $i from 0 through 10 {
$cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i);
$cos: $cos + pow(-1, $i) * math.div(pow($angle, 2 * $i), fact(2 * $i));
}
@return $cos;
@@ -132,5 +137,5 @@
// @return {number}
@function tan($angle) {
@return sin($angle) / cos($angle);
@return math.div(sin($angle), cos($angle));
}

View File

@@ -51,13 +51,13 @@
font-size: rem($font-size) $important;
@if ($line-height == "auto") {
line-height: ceil($font-size / $line-height) * ($line-height / $font-size) $important;
line-height: ceil(math.div($font-size, $line-height)) * math.div($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;
}
@elseif ($line-height != "none" and $line-height != false) {
@else if ($line-height != "none" and $line-height != false) {
@error "Doh! `#{$line-height}` is not a valid value for `$line-height`.";
}
}
@@ -193,3 +193,32 @@
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

@@ -58,7 +58,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: ($numerator / $denominator) * 100% $important;
width: math.div($numerator, $denominator) * 100% $important;
}
@if ($widths-offsets == true) {
@@ -66,13 +66,13 @@ $breakpoint-delimiter: \@ !default;
.u-push-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
position: relative $important;
right: auto $important;
left: ($numerator / $denominator) * 100% $important;
left: math.div($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: ($numerator / $denominator) * 100% $important;
right: math.div($numerator, $denominator) * 100% $important;
left: auto $important;
}
}

View File

@@ -11,51 +11,31 @@
$colsMax: $base-column-nb + 1;
$breakpoints: (
"null" null,
"from-tiny" "from-tiny",
"from-small" "from-small",
"from-medium" "from-medium",
"from-large" "from-large",
"from-big" "from-big"
) !default;
@each $breakpoint-namespace, $breakpoint in $breakpoints {
@each $breakpoint, $mediaquery in $breakpoints {
@for $fromIndex from 1 through $colsMax {
@for $toIndex from 1 through $colsMax {
@if $breakpoint == null {
// Columns without media query
@if $breakpoint == "tiny" {
.u-gc-#{$fromIndex}\/#{$toIndex} {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
} @else {
.u-gc-#{$fromIndex}\/#{$toIndex}\@#{$breakpoint} {
@if $breakpoint-namespace == "from-tiny" {
@media (min-width: $from-tiny) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-small" {
@media (min-width: $from-small) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-medium" {
@media (min-width: $from-medium) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-large" {
@media (min-width: $from-large) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-big" {
@media (min-width: $from-big) {
grid-column-start: #{$fromIndex};
grid-column-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

@@ -28,8 +28,8 @@ $aspect-ratios: (
@error "`#{$consequent}` needs to be a number."
}
&.u-#{$antecedent}\:#{$consequent}::before {
padding-bottom: ($consequent/$antecedent) * 100%;
.u-#{$antecedent}\:#{$consequent}::before {
padding-bottom: math.div($consequent, $antecedent) * 100%;
}
}
}

View File

@@ -26,8 +26,8 @@ $spacing-directions: (
'-right': '-right',
'-bottom': '-bottom',
'-left': '-left',
'-horizontal': '-left' '-right',
'-vertical': '-top' '-bottom',
'-x': '-left' '-right',
'-y': '-top' '-bottom',
) !default;
$spacing-properties: (
@@ -35,19 +35,47 @@ $spacing-properties: (
'margin': 'margin',
) !default;
$spacing-sizes: (
null: $unit,
'-double': $unit * 2,
'-small': $unit-small,
'-none': 0px
) !default;
$spacing-sizes: join($spacers, (
null: var(--grid-gutter),
'none': 0
));
@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;
@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};
// Spacer without media query
@if $breakpoint == "tiny" {
#{$base-class} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
// Spacer min-width breakpoints `@from-*`
#{$base-class}\@from-#{$breakpoint} {
@media #{mq-min($breakpoint)} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
// Spacer max-width breakpoints @to-*`
#{$base-class}\@to-#{$breakpoint} {
@media #{mq-max($breakpoint)} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
}
}

View File

@@ -23,6 +23,6 @@ $widths-fractions: 1 2 3 4 5 !default;
.u-1\/2\@from-small {
@media (min-width: $from-small) {
width: span(1/2);
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',
],
};

25
build/helpers/config.js Normal file
View File

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

162
build/helpers/glob.js Normal file
View File

@@ -0,0 +1,162 @@
/**
* @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

@@ -11,7 +11,7 @@ import kleur from 'kleur';
* @param {string} [type] - The type of message.
* @param {string} [timerID] - The console time label to output.
*/
export default function message(text, type, timerID) {
function message(text, type, timerID) {
switch (type) {
case 'success':
console.log('✅ ', kleur.bgGreen().black(text));
@@ -52,4 +52,10 @@ export default 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}
*/
export default function notification(options, callback) {
function notification(options, callback) {
if (typeof options === 'string') {
options = {
message: options
@@ -42,4 +42,10 @@ export default function notification(options, callback) {
}
notifier.notify(options, callback);
}
export default notification;
export {
notification,
};

139
build/helpers/postcss.js Normal file
View File

@@ -0,0 +1,139 @@
/**
* @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,
};

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

@@ -3,6 +3,10 @@
*/
import loconfig from './config.js';
import {
escapeRegExp,
flatten
} from '../utils/index.js';
const templateData = flatten({
paths: loconfig.paths
@@ -22,7 +26,7 @@ const templateData = flatten({
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {*} Returns the transformed value.
*/
export default function resolve(input, data = templateData) {
function resolve(input, data = templateData) {
switch (typeof input) {
case 'string': {
return resolveValue(input, data);
@@ -56,7 +60,7 @@ export default function resolve(input, data = templateData) {
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {string} Returns the translated string.
*/
export function resolveValue(input, data = templateData) {
function resolveValue(input, data = templateData) {
const tags = [];
if (data !== templateData) {
@@ -93,55 +97,9 @@ export function resolveValue(input, data = templateData) {
});
}
/**
* 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 default resolve;
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, '\\$&');
}
export {
resolve,
resolveValue,
};

View File

@@ -1,8 +1,9 @@
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 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 concat from 'concat';
import {
basename,
@@ -64,7 +65,7 @@ export const productionConcatFilesArgs = [
* @return {Promise}
*/
export default async function concatFiles(globOptions = null, concatOptions = null) {
if (glob) {
if (supportsGlob) {
if (globOptions == null) {
globOptions = productionGlobOptions;
} else if (
@@ -72,7 +73,7 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
globOptions !== developmentGlobOptions &&
globOptions !== productionGlobOptions
) {
globOptions = Object.assign({}, defaultGlobOptions, globOptions);
globOptions = merge({}, defaultGlobOptions, globOptions);
}
}
@@ -82,10 +83,19 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
concatOptions !== developmentConcatOptions &&
concatOptions !== productionConcatOptions
) {
concatOptions = Object.assign({}, defaultConcatOptions, concatOptions);
concatOptions = merge({}, defaultConcatOptions, concatOptions);
}
loconfig.tasks.concats.forEach(async ({
/**
* @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 ({
includes,
outfile,
label = null
@@ -98,25 +108,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);
let files;
if (glob && globOptions) {
files = await glob(includes, globOptions);
} else {
files = includes;
if (supportsGlob && globOptions) {
includes = await glob(includes, globOptions);
}
if (concatOptions.removeDuplicates) {
files = files.map((path) => normalize(path));
files = [ ...new Set(files) ];
includes = includes.map((path) => normalize(path));
includes = [ ...new Set(includes) ];
}
await concat(files, outfile);
await concat(includes, outfile);
if (files.length) {
if (includes.length) {
message(`${label} concatenated`, 'success', timeLabel);
} else {
message(`${label} is empty`, 'notice', timeLabel);

View File

@@ -1,7 +1,8 @@
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 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 esbuild from 'esbuild';
import { basename } from 'node:path';
@@ -50,10 +51,21 @@ export default async function compileScripts(esBuildOptions = null) {
esBuildOptions !== developmentESBuildOptions &&
esBuildOptions !== productionESBuildOptions
) {
esBuildOptions = Object.assign({}, defaultESBuildOptions, esBuildOptions);
esBuildOptions = merge({}, defaultESBuildOptions, esBuildOptions);
}
loconfig.tasks.scripts.forEach(async ({
/**
* @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 ({
includes,
outdir = '',
outfile = '',
@@ -67,6 +79,10 @@ 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,12 +1,17 @@
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 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 { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { promisify } from 'node:util';
import sass, { types } from 'node-sass';
import * as sass from 'sass';
import { PurgeCSS } from 'purgecss';
const sassRender = promisify(sass.render);
@@ -26,11 +31,6 @@ export const defaultSassOptions = {
export const developmentSassOptions = Object.assign({}, defaultSassOptions, {
outputStyle: 'expanded',
functions: {
'app-env()': function () {
return (new types.String('development'))
}
}
});
export const productionSassOptions = Object.assign({}, defaultSassOptions, {
outputStyle: 'compressed',
@@ -60,10 +60,12 @@ export const productionPostCSSOptions = Object.assign({}, defaultPostCSSOptions
export const developmentStylesArgs = [
developmentSassOptions,
developmentPostCSSOptions,
false
];
export const productionStylesArgs = [
productionSassOptions,
productionPostCSSOptions,
true
];
/**
@@ -80,17 +82,17 @@ export const productionStylesArgs = [
* If `false`, PostCSS processing will be ignored.
* @return {Promise}
*/
export default async function compileStyles(sassOptions = null, postcssOptions = null) {
export default async function compileStyles(sassOptions = null, postcssOptions = null, purge = true) {
if (sassOptions == null) {
sassOptions = productionSassOptions;
} else if (
sassOptions !== developmentSassOptions &&
sassOptions !== productionSassOptions
) {
sassOptions = Object.assign({}, defaultSassOptions, sassOptions);
sassOptions = merge({}, defaultSassOptions, sassOptions);
}
if (postcss) {
if (supportsPostCSS) {
if (postcssOptions == null) {
postcssOptions = productionPostCSSOptions;
} else if (
@@ -98,11 +100,20 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
postcssOptions !== developmentPostCSSOptions &&
postcssOptions !== productionPostCSSOptions
) {
postcssOptions = Object.assign({}, defaultPostCSSOptions, postcssOptions);
postcssOptions = merge({}, defaultPostCSSOptions, postcssOptions);
}
}
loconfig.tasks.styles.forEach(async ({
/**
* @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 ({
infile,
outfile,
label = null
@@ -121,9 +132,9 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
outFile: outfile,
}));
if (postcss && postcssOptions) {
if (supportsPostCSS && postcssOptions) {
if (typeof postcssProcessor === 'undefined') {
postcssProcessor = createPostCSSProcessor(
postcssProcessor = createProcessor(
postcssPluginsMap,
postcssOptions
);
@@ -151,7 +162,7 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
try {
await writeFile(outfile, result.css).then(() => {
// Purge CSS once file exists.
if (outfile) {
if (outfile && purge) {
purgeUnusedCSS(outfile, `${label || `${filestem}.css`}`);
}
});
@@ -196,35 +207,6 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
});
};
/**
* Creates a PostCSS Processor with the given plugins and options.
*
* @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.
*/
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);
}
}
return postcss(plugins);
}
/**
* Purge unused styles from CSS files.
*
@@ -236,23 +218,30 @@ function createPostCSSProcessor(pluginsListOrMap, options)
* @return {Promise}
*/
async function purgeUnusedCSS(outfile, label) {
const contentFiles = loconfig.tasks.purgeCSS?.content;
if (!Array.isArray(contentFiles) || !contentFiles.length) {
return;
}
label = label ?? basename(outfile);
const timeLabel = `${label} purged in`;
console.time(timeLabel);
const purgeCSSContentFiles = Array.from(loconfig.tasks.purgeCSS.content);
const purgeCSSResults = await new PurgeCSS().purge({
content: purgeCSSContentFiles,
const purgeCSSResults = await (new PurgeCSS()).purge({
content: contentFiles,
css: [ outfile ],
rejected: true,
defaultExtractor: content => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [],
fontFaces: true,
keyframes: true,
safelist: {
standard: [ /^((?!\bu-gc-).)*$/ ]
}
// Keep all except .u-gc-* | .u-margin-* | .u-padding-*
standard: [ /^(?!.*\b(u-gc-|u-margin|u-padding)).*$/ ]
},
variables: true,
})
for(let result of purgeCSSResults) {
for (let result of purgeCSSResults) {
await writeFile(outfile, result.css)
message(`${label} purged`, 'chore', timeLabel);

View File

@@ -1,7 +1,10 @@
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 defaultSVGOOptions from '../config/svgo.js';
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import optimize from '../helpers/svgo.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import { basename } from 'node:path';
import mixer from 'svg-mixer';
@@ -19,35 +22,79 @@ 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 (
mixerOptions !== developmentMixerOptions &&
mixerOptions !== productionMixerOptions
) {
mixerOptions = Object.assign({}, defaultMixerOptions, mixerOptions);
mixerOptions = merge({}, defaultMixerOptions, mixerOptions);
}
loconfig.tasks.svgs.forEach(async ({
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.
* @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 ({
includes,
outfile,
label = null
@@ -60,11 +107,19 @@ export default async function compileSVGs(mixerOptions = null) {
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
outfile = resolve(outfile);
const result = await mixer(includes, mixerOptions);
if (optimize && svgoOptions) {
result.content = optimize(result.content, svgoOptions).data;
}
await result.write(outfile);
message(`${label} compiled`, 'success', timeLabel);

View File

@@ -1,6 +1,7 @@
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import resolve from '../utils/template.js';
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 {
@@ -20,6 +21,8 @@ import {
} 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
@@ -94,12 +97,23 @@ export default async function bumpVersions(versionOptions = null) {
versionOptions !== developmentVersionOptions &&
versionOptions !== productionVersionOptions
) {
versionOptions = Object.assign({}, defaultVersionOptions, versionOptions);
versionOptions = merge({}, defaultVersionOptions, versionOptions);
}
const queue = new Map();
loconfig.tasks.versions.forEach(({
/**
* @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
@@ -123,32 +137,64 @@ export default async function bumpVersions(versionOptions = null) {
/**
* Creates a formatted version number or string.
*
* @param {string} format - The version format.
* @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) {
function createVersionNumber(format, oldValue = null) {
let [ type, modifier ] = format.split(':', 2);
switch (type) {
case 'hex':
case 'hexadecimal':
modifier = Number.parseInt(modifier);
try {
modifier = Number.parseInt(modifier);
if (Number.isNaN(modifier)) {
modifier = 6;
if (Number.isNaN(modifier)) {
modifier = 6;
}
return randomBytes(modifier).toString('hex');
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "hexadecimal"`,
{ cause: err }
);
}
return randomBytes(modifier).toString('hex');
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':
return new RegExp(modifier);
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" or "hexadecimal"'
'Expected \'format\' to be either "timestamp", "increment", or "hexadecimal"'
);
}
@@ -209,9 +255,7 @@ async function handleBumpVersionInJson(outfile, label, options) {
await mkdir(dirname(outfile), { recursive: true });
}
const version = createVersionNumber(options.format);
json[options.key] = version;
json[options.key] = createVersionNumber(options.format, json?.[options.key]);
return await writeFile(
outfile,
@@ -242,7 +286,7 @@ async function handleBumpVersionWithRegExp(outfile, label, options) {
input: createReadStream(bckfile),
});
const version = createVersionNumber(options.format);
let newVersion = null;
const writeStream = createWriteStream(outfile, { encoding: 'utf8' });
@@ -250,12 +294,12 @@ async function handleBumpVersionWithRegExp(outfile, label, options) {
const found = line.match(options.key);
if (found) {
const groups = (found.groups ?? {});
const replace = found[0].replace(
(groups.build ?? groups.version ?? found[1] ?? found[0]),
version
);
line = line.replace(found[0], replace);
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");
@@ -273,6 +317,88 @@ async function handleBumpVersionWithRegExp(outfile, label, options) {
}
}
/**
* 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.
*

View File

@@ -1,64 +0,0 @@
/**
* @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');
}

View File

@@ -1,95 +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}
* - {@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);
});
};
}

115
build/utils/index.js Normal file
View File

@@ -0,0 +1,115 @@
/**
* @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

@@ -1,27 +0,0 @@
/**
* @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

@@ -2,10 +2,11 @@ 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, { merge } from './utils/config.js';
import message from './utils/message.js';
import notification from './utils/notification.js';
import resolve from './utils/template.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 browserSync from 'browser-sync';
import { join } from 'node:path';
@@ -69,16 +70,18 @@ function configureServer(server, { paths, tasks }) {
});
// Watch source concats
server.watch(
resolve(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
if (tasks.concats?.length) {
server.watch(
resolve(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
)
)
)
).on('change', () => {
concatFiles(...developmentConcatFilesArgs);
});
).on('change', () => {
concatFiles(...developmentConcatFilesArgs);
});
}
// Watch source styles
server.watch(

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)
@@ -265,7 +268,7 @@ See [`scripts.js`](../build/tasks/scripts.js) for details.
### `styles`
A wrapper around [node-sass] (with optional support for [Autoprefixer]
A wrapper around [sass] (with optional support for [Autoprefixer]
via [PostCSS]) for compiling and minifying Sass into CSS.
By default, [PostCSS] and [Autoprefixer] are installed with the boilerplate.
@@ -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:
@@ -338,8 +341,8 @@ See [`svgs.js`](../build/tasks/svgs.js) for details.
A task to create and update values for use in versioning assets.
Can generate a hexadecimal value (using random bytes) or
use the current timestamp.
Can generate a hexadecimal value (using random bytes), use the current timestamp,
or increment a number.
Example:
@@ -355,6 +358,11 @@ Example:
"format": "hex:8",
"key": "hex",
"outfile": "./assets.json"
},
{
"format": "inc:semver",
"key": "inc",
"outfile": "./assets.json"
}
]
}
@@ -363,7 +371,8 @@ Example:
```json
{
"now": 1665071717350,
"hex": "6ef54181c4ba"
"hex": "6ef54181c4ba",
"hex": "1.0.2"
}
```
@@ -416,11 +425,12 @@ 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/
[node-sass]: https://npmjs.com/package/node-sass
[sass]: https://npmjs.com/package/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
[SVGO]: https://npmjs.com/package/svgo
[tiny-glob]: https://npmjs.com/package/tiny-glob

View File

@@ -181,10 +181,7 @@ detection and smooth scrolling with parallax.
```js
import LocomotiveScroll from 'locomotive-scroll';
this.scroll = new LocomotiveScroll({
el: this.el,
smooth: true
});
this.scroll = new LocomotiveScroll({})
````
Learn more about [Locomotive Scroll][locomotive-scroll].

6789
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,34 +6,39 @@
"author": "Locomotive <info@locomotive.ca>",
"type": "module",
"engines": {
"node": ">=14.17",
"node": ">=20.1",
"npm": ">=8.0"
},
"scripts": {
"start": "node --experimental-json-modules --no-warnings build/watch.js",
"build": "node --experimental-json-modules --no-warnings build/build.js"
"build": "node --experimental-json-modules --no-warnings build/build.js",
"optimize:svg": "svgo --config=build/config/svgo.js"
},
"dependencies": {
"locomotive-scroll": "^4.1.4",
"locomotive-scroll": "^5.0.0-beta.11",
"modujs": "^1.4.2",
"modularload": "^1.2.6",
"normalize.css": "^8.0.1",
"svg4everybody": "^2.1.9"
},
"devDependencies": {
"autoprefixer": "^10.4.13",
"browser-sync": "^2.27.11",
"autoprefixer": "^10.4.17",
"browser-sync": "^3.0.2",
"concat": "^1.0.3",
"esbuild": "^0.16.17",
"esbuild": "^0.20.0",
"kleur": "^4.1.5",
"node-notifier": "^10.0.1",
"node-sass": "^8.0.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"
},
"svg-mixer": {
"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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -18,8 +18,8 @@
</head>
<body data-module-load>
<div data-load-container>
<div class="o-scroll" data-module-scroll="main">
<header data-scroll-section>
<div data-module-scroll="main">
<header>
<a href="/"><h1>Locomotive Boilerplate</h1></a>
<nav>
<ul>
@@ -30,7 +30,7 @@
</nav>
</header>
<main data-scroll-section>
<main>
<div class="o-container">
<h1 class="c-heading -h1">Page</h1>
@@ -90,7 +90,7 @@
</div>
</main>
<footer data-scroll-section>
<footer>
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate" title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
</footer>
</div>

View File

@@ -34,8 +34,8 @@
<body data-module-load>
<div data-load-container>
<div class="o-scroll" data-module-scroll="main">
<header data-scroll-section>
<div data-module-scroll="main">
<header>
<a href="/">
<h1>Locomotive Boilerplate</h1>
</a>
@@ -48,7 +48,7 @@
</nav>
</header>
<main data-scroll-section>
<main>
<div class="o-container">
<h1 class="c-heading -h1">Hello</h1>
@@ -76,7 +76,7 @@
</div>
</main>
<footer data-scroll-section>
<footer>
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate"
title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
</footer>

View File

@@ -18,8 +18,8 @@
</head>
<body data-module-load>
<div data-load-container>
<div class="o-scroll" data-module-scroll="main">
<header data-scroll-section>
<div data-module-scroll="main">
<header>
<a href="/"><h1>Locomotive Boilerplate</h1></a>
<nav>
<ul>
@@ -30,7 +30,7 @@
</nav>
</header>
<main data-scroll-section>
<main>
<div class="o-container">
<h1 class="c-heading -h1">Images</h1>
@@ -116,7 +116,7 @@
</div>
</main>
<footer data-scroll-section>
<footer>
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate" title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
</footer>
</div>

View File

@@ -42,8 +42,8 @@
<body data-module-load>
<div data-load-container>
<div class="o-scroll" data-module-scroll="main">
<header data-scroll-section>
<div data-module-scroll="main">
<header>
<a href="/">
<h1>Locomotive Boilerplate</h1>
</a>
@@ -56,13 +56,13 @@
</nav>
</header>
<main data-module-example data-scroll-section>
<main data-module-example>
<div class="o-container">
<h1 class="c-heading -h1">Hello</h1>
</div>
</main>
<footer data-scroll-section>
<footer>
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate"
title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
</footer>