mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Compare commits
31 Commits
mcaskill/j
...
feature/ty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b026bdb809 | ||
|
|
53beae26b0 | ||
|
|
9a01c0f17f | ||
|
|
97d9f1ec00 | ||
|
|
8b8b267e9d | ||
|
|
822cf7daa8 | ||
|
|
7f452f1fcc | ||
|
|
5010560ee3 | ||
|
|
0cfb3fbc7d | ||
|
|
86f88c3f14 | ||
|
|
48bd911804 | ||
|
|
d49d3eabb2 | ||
|
|
28aa6c7de6 | ||
|
|
38dd28832e | ||
|
|
757c26c772 | ||
|
|
5e07473396 | ||
|
|
c9056b27d8 | ||
|
|
38a6c73d2f | ||
|
|
9d18205b0f | ||
|
|
1e7e90c8aa | ||
|
|
47007cddaf | ||
|
|
5dd3fa843f | ||
|
|
a5623d3122 | ||
|
|
67e1fae8f4 | ||
|
|
a95fe4523c | ||
|
|
eadc414329 | ||
|
|
b19c18b18c | ||
|
|
db85740a18 | ||
|
|
5c24fabaa2 | ||
|
|
9e3d304654 | ||
|
|
6ded72bc79 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
loconfig.*.json
|
||||
!loconfig.example.json
|
||||
|
||||
257
README.md
257
README.md
@@ -4,230 +4,95 @@
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">Locomotive Boilerplate</h1>
|
||||
<p align="center">Front-end boilerplate for projects by Locomotive.</p>
|
||||
<p align="center">Front-end boilerplate for projects by <a href="https://locomotive.ca/">Locomotive</a>.</p>
|
||||
|
||||
## Requirements
|
||||
## Features
|
||||
|
||||
| Name | Version |
|
||||
| ---------- | -------- |
|
||||
| [Node] | >= 14.17 |
|
||||
| [NPM] | >= 6.0 |
|
||||
* Uses a custom [task runner](docs/development.md) for handling assets.
|
||||
* Uses [BrowserSync] for fast development and testing in browsers.
|
||||
* Uses [Sass] for a feature rich superset of CSS.
|
||||
* Uses [ESBuild] for extremely fast processing of JS/ES modules.
|
||||
* Uses [SVG Mixer] for processing SVG files and generating spritesheets.
|
||||
* Uses [ITCSS] for a sane and scalable CSS architecture.
|
||||
* Uses [Locomotive Scroll] for smooth scrolling with parallax effects.
|
||||
|
||||
[Node]: https://nodejs.org/
|
||||
[NPM]: https://npmjs.com/
|
||||
Learn more about [languages and technologies](docs/technologies.md).
|
||||
|
||||
You can use [nvm](https://github.com/nvm-sh/nvm) to install the node version in `.nvmrc`.
|
||||
## Getting started
|
||||
|
||||
## Installation
|
||||
Make sure you have the following installed:
|
||||
|
||||
* [Node] — at least 14.17, the latest LTS is recommended.
|
||||
* [NPM] — at least 6.0, the latest LTS is recommended.
|
||||
|
||||
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
|
||||
|
||||
```sh
|
||||
npm i
|
||||
# Clone the repository.
|
||||
git clone https://github.com/locomotivemtl/locomotive-boilerplate.git my-new-project
|
||||
|
||||
# Enter the newly-cloned directory.
|
||||
cd my-new-project
|
||||
```
|
||||
|
||||
## Usage
|
||||
Then replace the original remote repository with your project's repository.
|
||||
|
||||
```sh
|
||||
# start it
|
||||
npm start
|
||||
```
|
||||
Then update the following files to suit your project:
|
||||
|
||||
## Configuration
|
||||
|
||||
There are a few occurrences that should be renamed for your project:
|
||||
|
||||
* [package.json](package.json):
|
||||
* [`README.md`](README.md):
|
||||
The file you are currently reading.
|
||||
* [`package.json`](package.json):
|
||||
* Package name: `@locomotivemtl/boilerplate`
|
||||
* Package title: `Locomotive Boilerplate`
|
||||
* [package-lock.json](package-lock.json):
|
||||
* [`package-lock.json`](package-lock.json):
|
||||
* Package name: `@locomotivemtl/boilerplate`
|
||||
* [loconfig.json](loconfig.json):
|
||||
* Browser Sync proxy URL: `locomotive-boilerplate.test`
|
||||
Remove `paths.url` to use Browser Sync's built-in server which uses `paths.dest`.
|
||||
* [`loconfig.json`](loconfig.json):
|
||||
* BrowserSync proxy URL: `locomotive-boilerplate.test`
|
||||
Remove `paths.url` to use BrowserSync's built-in server which uses `paths.dest`.
|
||||
* View path: `./views/boilerplate/template`
|
||||
* [environment.js](assets/scripts/utils/environment.js):
|
||||
* [`environment.js`](assets/scripts/utils/environment.js):
|
||||
* Application name: `Boilerplate`
|
||||
* [site.webmanifest](www/site.webmanifest):
|
||||
* [`site.webmanifest`](www/site.webmanifest):
|
||||
* Manifest name: `Locomotive Boilerplate`
|
||||
* Manifest short name: `Boilerplate`
|
||||
* HTML files:
|
||||
* Page title: `Locomotive Boilerplate`
|
||||
|
||||
## Build
|
||||
## Installation
|
||||
|
||||
#### Tasks
|
||||
```sh
|
||||
# watch
|
||||
# Switch to recommended Node version from .nvmrc
|
||||
nvm use
|
||||
|
||||
# Install dependencies from package.json
|
||||
npm install
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
# Start development server, watch for changes, and compile assets
|
||||
npm start
|
||||
|
||||
# build
|
||||
# Compile and minify assets
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Styles
|
||||
Learn more about [development and building](docs/development.md).
|
||||
|
||||
[Sass](https://github.com/sass/node-sass) is our CSS preprocessor. [Autoprefixer](https://github.com/postcss/autoprefixer) is also included.
|
||||
## Documentation
|
||||
|
||||
#### Architecture
|
||||
* [Development and building](docs/development.md)
|
||||
* [Languages and technologies](docs/technologies.md)
|
||||
|
||||
[ITCSS](https://github.com/itcss) is our CSS architecture.
|
||||
|
||||
* `settings`: Global variables, site-wide settings, config switches, etc.
|
||||
* `tools`: Site-wide mixins and functions.
|
||||
* `generic`: Low-specificity, far-reaching rulesets (e.g. resets).
|
||||
* `elements`: Unclassed HTML elements (e.g. `a {}`, `blockquote {}`, `address {}`).
|
||||
* `objects`: Objects, abstractions, and design patterns (e.g. `.o-layout {}`).
|
||||
* `components`: Discrete, complete chunks of UI (e.g. `.c-carousel {}`).
|
||||
* `utilities`: High-specificity, very explicit selectors. Overrides and helper
|
||||
classes (e.g. `.u-hidden {}`)
|
||||
|
||||
[_source_](https://github.com/inuitcss/inuitcss#css-directory-structure)
|
||||
|
||||
#### Naming
|
||||
|
||||
We use a simplified [BEM](https://github.com/bem) syntax.
|
||||
|
||||
```
|
||||
.block .block_element -modifier
|
||||
```
|
||||
|
||||
#### Namespaces
|
||||
|
||||
We namespace our classes for more [transparency](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/).
|
||||
|
||||
* `o-`: Object that it may be used in any number of unrelated contexts to the one you can currently see it in. Making modifications to these types of class could potentially have knock-on effects in a lot of other unrelated places.
|
||||
* `c-`: Component is a concrete, implementation-specific piece of UI. All of the changes you make to its styles should be detectable in the context you’re currently looking at. Modifying these styles should be safe and have no side effects.
|
||||
* `u-`: Utility has a very specific role (often providing only one declaration) and should not be bound onto or changed. It can be reused and is not tied to any specific piece of UI.
|
||||
* `s-`: Scope creates a new styling context. Similar to a Theme, but not necessarily cosmetic, these should be used sparingly—they can be open to abuse and lead to poor CSS if not used wisely.
|
||||
* `is-`, `has-`: Is currently styled a certain way because of a state or condition. It tells us that the DOM currently has a temporary, optional, or short-lived style applied to it due to a certain state being invoked.
|
||||
|
||||
[_source_](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/#the-namespaces)
|
||||
|
||||
#### Example
|
||||
|
||||
```html
|
||||
<div class="c-block -large">
|
||||
<div class="c-block_layout o-layout">
|
||||
<div class="o-layout_item u-1/2@from-medium">
|
||||
<div class="c-block_heading o-h -medium">Heading</div>
|
||||
</div>
|
||||
<div class="o-layout_item u-1/2@from-medium">
|
||||
<a class="c-block_button o-button -outline" href="#">Button</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
```scss
|
||||
.c-block {
|
||||
&.-large {
|
||||
padding: rem(60px);
|
||||
}
|
||||
}
|
||||
|
||||
.c-block_heading {
|
||||
@media (max-width: $to-medium) {
|
||||
.c-block.-large & {
|
||||
margin-bottom: rem(40px);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
[modularJS](https://github.com/modularorg/modularjs) is a small framework we use on top of ES modules. It compiles with [Rollup](https://github.com/rollup/rollup) and [Babel](https://github.com/babel/babel).
|
||||
|
||||
#### Why
|
||||
|
||||
* Automatically init visible modules.
|
||||
* Easily call other modules methods.
|
||||
* Quickly set scoped events with delegation.
|
||||
* Simply select DOM elements scoped in their module.
|
||||
|
||||
[_source_](https://github.com/modularorg/modularjs#why)
|
||||
|
||||
#### Example
|
||||
|
||||
```html
|
||||
<div data-module-example>
|
||||
<div data-example="main">
|
||||
<h2>Example</h2>
|
||||
</div>
|
||||
<button data-example="load">More</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import { module } from 'modujs';
|
||||
|
||||
export default class extends module {
|
||||
constructor(m) {
|
||||
super(m);
|
||||
|
||||
this.events = {
|
||||
click: {
|
||||
load: 'loadMore'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.$('main')[0].classList.add('is-loading');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more](https://github.com/modularorg/modularjs)
|
||||
|
||||
## Page transitions
|
||||
|
||||
[modularLoad](https://github.com/modularorg/modularload) is used for page transitions and lazy loading.
|
||||
|
||||
#### Example
|
||||
|
||||
```html
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/page" data-load="transitionName">Page</a>
|
||||
</nav>
|
||||
<div data-load-container>
|
||||
<img data-load-src="assets/images/hello.jpg">
|
||||
</div>
|
||||
```
|
||||
```js
|
||||
import modularLoad from 'modularload';
|
||||
|
||||
this.load = new modularLoad({
|
||||
enterDelay: 300,
|
||||
transitions: {
|
||||
transitionName: {
|
||||
enterDelay: 450
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
[Learn more](https://github.com/modularorg/modularload)
|
||||
|
||||
## Scroll detection
|
||||
|
||||
[Locomotive Scroll](https://github.com/locomotivemtl/locomotive-scroll) is used for elements in viewport detection and smooth scrolling with parallax.
|
||||
|
||||
#### Example
|
||||
|
||||
```html
|
||||
<div data-module-scroll>
|
||||
<div data-scroll>Trigger</div>
|
||||
<div data-scroll data-scroll-speed="1">Parallax</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import LocomotiveScroll from 'locomotive-scroll';
|
||||
|
||||
this.scroll = new LocomotiveScroll({
|
||||
el: this.el,
|
||||
smooth: true
|
||||
});
|
||||
````
|
||||
|
||||
[Learn more](https://github.com/locomotivemtl/locomotive-scroll)
|
||||
[BrowserSync]: https://npmjs.com/package/browser-sync
|
||||
[ESBuild]: https://npmjs.com/package/esbuild
|
||||
[ITCSS]: https://itcss.io/
|
||||
[Locomotive Scroll]: https://npmjs.com/package/locomotive-scroll
|
||||
[modularJS]: https://npmjs.com/package/modujs
|
||||
[modularLoad]: https://npmjs.com/package/modularload
|
||||
[Sass]: https://sass-lang.com/
|
||||
[SVG Mixer]: https://npmjs.com/package/svg-mixer
|
||||
[Node]: https://nodejs.org/
|
||||
[NPM]: https://npmjs.com/
|
||||
[NVM]: https://github.com/nvm-sh/nvm
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
.c-heading {
|
||||
line-height: $line-height-h;
|
||||
margin-bottom: rem(30px);
|
||||
|
||||
&.-h1 {
|
||||
font-size: rem($font-size-h1);
|
||||
}
|
||||
|
||||
&.-h2 {
|
||||
font-size: rem($font-size-h2);
|
||||
}
|
||||
|
||||
&.-h3 {
|
||||
font-size: rem($font-size-h3);
|
||||
}
|
||||
|
||||
&.-h4 {
|
||||
font-size: rem($font-size-h4);
|
||||
}
|
||||
|
||||
&.-h5 {
|
||||
font-size: rem($font-size-h5);
|
||||
}
|
||||
|
||||
&.-h6 {
|
||||
font-size: rem($font-size-h6);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,17 @@
|
||||
// Base / Page
|
||||
// ==========================================================================
|
||||
|
||||
:root {
|
||||
|
||||
// Font sizes
|
||||
--font-size-h1: #{rem(36px)};
|
||||
--font-size-h2: #{rem(28px)};
|
||||
--font-size-h3: #{rem(24px)};
|
||||
--font-size-h4: #{rem(20px)};
|
||||
--font-size-h5: #{rem(18px)};
|
||||
--font-size-h6: #{rem(16px)};
|
||||
}
|
||||
|
||||
//
|
||||
// Simple page-level setup.
|
||||
//
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
@import "elements/fonts";
|
||||
@import "elements/page";
|
||||
|
||||
// Typography
|
||||
// ==========================================================================
|
||||
@import "typography/headings";
|
||||
@import "typography/wysiwyg";
|
||||
|
||||
// Objects
|
||||
// ==========================================================================
|
||||
@import "objects/scroll";
|
||||
@@ -47,7 +52,6 @@
|
||||
// Components
|
||||
// ==========================================================================
|
||||
@import "components/scrollbar";
|
||||
@import "components/heading";
|
||||
@import "components/button";
|
||||
@import "components/form";
|
||||
|
||||
|
||||
@@ -21,14 +21,6 @@ $font-size: 16px;
|
||||
$line-height: 24px / $font-size;
|
||||
$font-family: $font-sans-serif;
|
||||
$color: #222222;
|
||||
// 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;
|
||||
// Weights
|
||||
$light: 300;
|
||||
$normal: 400;
|
||||
|
||||
38
assets/styles/typography/_headings.scss
Normal file
38
assets/styles/typography/_headings.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
// ==========================================================================
|
||||
// Typography / Headings
|
||||
// ==========================================================================
|
||||
|
||||
.t-h1,
|
||||
.t-h2,
|
||||
.t-h3,
|
||||
.t-h4,
|
||||
.t-h5,
|
||||
.t-h6 {
|
||||
--heading-line-height: #{$line-height};
|
||||
|
||||
line-height: var(--heading-line-height);
|
||||
}
|
||||
|
||||
.t-h1 {
|
||||
font-size: var(--font-size-h1);
|
||||
}
|
||||
|
||||
.t-h2 {
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
.t-h3 {
|
||||
font-size: var(--font-size-h3);
|
||||
}
|
||||
|
||||
.t-h4 {
|
||||
font-size: var(--font-size-h4);
|
||||
}
|
||||
|
||||
.t-h5 {
|
||||
font-size: var(--font-size-h5);
|
||||
}
|
||||
|
||||
.t-h6 {
|
||||
font-size: var(--font-size-h6);
|
||||
}
|
||||
55
assets/styles/typography/_wysiwyg.scss
Normal file
55
assets/styles/typography/_wysiwyg.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
// ==========================================================================
|
||||
// Typography / Wysiwyg
|
||||
// ==========================================================================
|
||||
|
||||
.t-wysiwyg {
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin-bottom: $line-height * 1em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 1em 0 0.5em;
|
||||
}
|
||||
|
||||
h1 { @extend .t-h1; }
|
||||
h2 { @extend .t-h2; }
|
||||
h3 { @extend .t-h3; }
|
||||
h4 { @extend .t-h4; }
|
||||
h5 { @extend .t-h5; }
|
||||
h6 { @extend .t-h6; }
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,132 @@
|
||||
import loconfig from '../../loconfig.json';
|
||||
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 template from '../utils/template.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import concat from 'concat';
|
||||
import { basename } from 'node:path';
|
||||
import {
|
||||
basename,
|
||||
normalize,
|
||||
} from 'node:path';
|
||||
|
||||
/**
|
||||
* @const {object} defaultGlobOptions - The default shared glob options.
|
||||
* @const {object} developmentGlobOptions - The predefined glob options for development.
|
||||
* @const {object} productionGlobOptions - The predefined glob options for production.
|
||||
*/
|
||||
export const defaultGlobOptions = {
|
||||
};
|
||||
export const developmentGlobOptions = Object.assign({}, defaultGlobOptions);
|
||||
export const productionGlobOptions = Object.assign({}, defaultGlobOptions);
|
||||
|
||||
/**
|
||||
* @typedef {object} ConcatOptions
|
||||
* @property {boolean} removeDuplicates - Removes duplicate paths from
|
||||
* the array of matching files and folders.
|
||||
* Only the first occurrence of each path is kept.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @const {ConcatOptions} defaultConcatOptions - The default shared concatenation options.
|
||||
* @const {ConcatOptions} developmentConcatOptions - The predefined concatenation options for development.
|
||||
* @const {ConcatOptions} productionConcatOptions - The predefined concatenation options for production.
|
||||
*/
|
||||
export const defaultConcatOptions = {
|
||||
removeDuplicates: true,
|
||||
};
|
||||
export const developmentConcatOptions = Object.assign({}, defaultConcatOptions);
|
||||
export const productionConcatOptions = Object.assign({}, defaultConcatOptions);
|
||||
|
||||
/**
|
||||
* @const {object} developmentConcatFilesArgs - The predefined `concatFiles()` options for development.
|
||||
* @const {object} productionConcatFilesArgs - The predefined `concatFiles()` options for production.
|
||||
*/
|
||||
export const developmentConcatFilesArgs = [
|
||||
developmentGlobOptions,
|
||||
developmentConcatOptions,
|
||||
];
|
||||
export const productionConcatFilesArgs = [
|
||||
productionGlobOptions,
|
||||
productionConcatOptions,
|
||||
];
|
||||
|
||||
/**
|
||||
* Concatenates groups of files.
|
||||
*
|
||||
* @todo Add support for minification.
|
||||
*
|
||||
* @async
|
||||
* @param {object|boolean} [globOptions=null] - Customize the glob options.
|
||||
* If `null`, default production options are used.
|
||||
* If `false`, the glob function will be ignored.
|
||||
* @param {object} [concatOptions=null] - Customize the concatenation options.
|
||||
* If `null`, default production options are used.
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default async function concatFiles() {
|
||||
export default async function concatFiles(globOptions = null, concatOptions = null) {
|
||||
if (glob) {
|
||||
if (globOptions == null) {
|
||||
globOptions = productionGlobOptions;
|
||||
} else if (
|
||||
globOptions !== false &&
|
||||
globOptions !== developmentGlobOptions &&
|
||||
globOptions !== productionGlobOptions
|
||||
) {
|
||||
globOptions = Object.assign({}, defaultGlobOptions, globOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (concatOptions == null) {
|
||||
concatOptions = productionConcatOptions;
|
||||
} else if (
|
||||
concatOptions !== developmentConcatOptions &&
|
||||
concatOptions !== productionConcatOptions
|
||||
) {
|
||||
concatOptions = Object.assign({}, defaultConcatOptions, concatOptions);
|
||||
}
|
||||
|
||||
loconfig.tasks.concats.forEach(async ({
|
||||
includes,
|
||||
outfile
|
||||
outfile,
|
||||
label = null
|
||||
}) => {
|
||||
const filename = basename(outfile || 'undefined');
|
||||
if (!label) {
|
||||
label = basename(outfile || 'undefined');
|
||||
}
|
||||
|
||||
const timeLabel = `${filename} concatenated in`;
|
||||
const timeLabel = `${label} concatenated in`;
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
includes = includes.map((path) => template(path));
|
||||
outfile = template(outfile);
|
||||
includes = resolve(includes);
|
||||
outfile = resolve(outfile);
|
||||
|
||||
const files = await glob(includes);
|
||||
let files;
|
||||
|
||||
if (glob && globOptions) {
|
||||
files = await glob(includes, globOptions);
|
||||
} else {
|
||||
files = includes;
|
||||
}
|
||||
|
||||
if (concatOptions.removeDuplicates) {
|
||||
files = files.map((path) => normalize(path));
|
||||
files = [ ...new Set(files) ];
|
||||
}
|
||||
|
||||
await concat(files, outfile);
|
||||
|
||||
if (files.length) {
|
||||
message(`${filename} concatenated`, 'success', timeLabel);
|
||||
message(`${label} concatenated`, 'success', timeLabel);
|
||||
} else {
|
||||
message(`${filename} is empty`, 'notice', timeLabel);
|
||||
message(`${label} is empty`, 'notice', timeLabel);
|
||||
}
|
||||
} catch (err) {
|
||||
message(`Error concatenating ${filename}`, 'error');
|
||||
message(`Error concatenating ${label}`, 'error');
|
||||
message(err);
|
||||
|
||||
notification({
|
||||
title: `${filename} concatenation failed 🚨`,
|
||||
title: `${label} concatenation failed 🚨`,
|
||||
message: `${err.name}: ${err.message}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,59 +1,95 @@
|
||||
import loconfig from '../../loconfig.json';
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import template from '../utils/template.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import esbuild from 'esbuild';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
/**
|
||||
* @const {object} defaultESBuildOptions - The default shared ESBuild options.
|
||||
* @const {object} developmentESBuildOptions - The predefined ESBuild options for development.
|
||||
* @const {object} productionESBuildOptions - The predefined ESBuild options for production.
|
||||
*/
|
||||
export const defaultESBuildOptions = {
|
||||
bundle: true,
|
||||
color: true,
|
||||
sourcemap: true,
|
||||
target: [
|
||||
'es2015',
|
||||
],
|
||||
};
|
||||
export const developmentESBuildOptions = Object.assign({}, defaultESBuildOptions);
|
||||
export const productionESBuildOptions = Object.assign({}, defaultESBuildOptions, {
|
||||
logLevel: 'warning',
|
||||
minify: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* @const {object} developmentScriptsArgs - The predefined `compileScripts()` options for development.
|
||||
* @const {object} productionScriptsArgs - The predefined `compileScripts()` options for production.
|
||||
*/
|
||||
export const developmentScriptsArgs = [
|
||||
developmentESBuildOptions,
|
||||
];
|
||||
export const productionScriptsArgs = [
|
||||
productionESBuildOptions,
|
||||
];
|
||||
|
||||
/**
|
||||
* Bundles and minifies main JavaScript files.
|
||||
*
|
||||
* @async
|
||||
* @param {object} [esBuildOptions=null] - Customize the ESBuild build API options.
|
||||
* If `null`, default production options are used.
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default async function compileScripts() {
|
||||
export default async function compileScripts(esBuildOptions = null) {
|
||||
if (esBuildOptions == null) {
|
||||
esBuildOptions = productionESBuildOptions;
|
||||
} else if (
|
||||
esBuildOptions !== developmentESBuildOptions &&
|
||||
esBuildOptions !== productionESBuildOptions
|
||||
) {
|
||||
esBuildOptions = Object.assign({}, defaultESBuildOptions, esBuildOptions);
|
||||
}
|
||||
|
||||
loconfig.tasks.scripts.forEach(async ({
|
||||
includes,
|
||||
outdir = '',
|
||||
outfile = ''
|
||||
outfile = '',
|
||||
label = null
|
||||
}) => {
|
||||
const filename = basename(outdir || outfile || 'undefined');
|
||||
if (!label) {
|
||||
label = basename(outdir || outfile || 'undefined');
|
||||
}
|
||||
|
||||
const timeLabel = `${filename} compiled in`;
|
||||
const timeLabel = `${label} compiled in`;
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
includes = includes.map((path) => template(path));
|
||||
includes = resolve(includes);
|
||||
|
||||
if (outdir) {
|
||||
outdir = template(outdir);
|
||||
outdir = resolve(outdir);
|
||||
} else if (outfile) {
|
||||
outfile = template(outfile);
|
||||
outfile = resolve(outfile);
|
||||
} else {
|
||||
throw new TypeError(
|
||||
'Expected \'outdir\' or \'outfile\''
|
||||
);
|
||||
}
|
||||
|
||||
await esbuild.build({
|
||||
await esbuild.build(Object.assign({}, esBuildOptions, {
|
||||
entryPoints: includes,
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
color: true,
|
||||
logLevel: 'error',
|
||||
target: [
|
||||
'es2015',
|
||||
],
|
||||
outdir,
|
||||
outfile
|
||||
});
|
||||
outfile,
|
||||
}));
|
||||
|
||||
message(`${filename} compiled`, 'success', timeLabel);
|
||||
message(`${label} compiled`, 'success', timeLabel);
|
||||
} catch (err) {
|
||||
// errors managments (already done in esbuild)
|
||||
notification({
|
||||
title: `${filename} compilation failed 🚨`,
|
||||
title: `${label} compilation failed 🚨`,
|
||||
message: `${err.errors[0].text} in ${err.errors[0].location.file} line ${err.errors[0].location.line}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import loconfig from '../../loconfig.json';
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import postcss from '../utils/postcss.js';
|
||||
import template from '../utils/template.js';
|
||||
import postcss, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { basename } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
@@ -10,50 +10,130 @@ import sass from 'node-sass';
|
||||
|
||||
const sassRender = promisify(sass.render);
|
||||
|
||||
let postcssProcessor;
|
||||
|
||||
/**
|
||||
* @const {object} defaultSassOptions - The default shared Sass options.
|
||||
* @const {object} developmentSassOptions - The predefined Sass options for development.
|
||||
* @const {object} productionSassOptions - The predefined Sass options for production.
|
||||
*/
|
||||
export const defaultSassOptions = {
|
||||
omitSourceMapUrl: true,
|
||||
sourceMap: true,
|
||||
sourceMapContents: true,
|
||||
};
|
||||
export const developmentSassOptions = Object.assign({}, defaultSassOptions, {
|
||||
outputStyle: 'expanded',
|
||||
});
|
||||
export const productionSassOptions = Object.assign({}, defaultSassOptions, {
|
||||
outputStyle: 'compressed',
|
||||
});
|
||||
|
||||
/**
|
||||
* @const {object} defaultPostCSSOptions - The default shared PostCSS options.
|
||||
* @const {object} developmentPostCSSOptions - The predefined PostCSS options for development.
|
||||
* @const {object} productionPostCSSOptions - The predefined PostCSS options for production.
|
||||
*/
|
||||
export const defaultPostCSSOptions = {
|
||||
processor: {
|
||||
map: {
|
||||
annotation: false,
|
||||
inline: false,
|
||||
sourcesContent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const developmentPostCSSOptions = Object.assign({}, defaultPostCSSOptions);
|
||||
export const productionPostCSSOptions = Object.assign({}, defaultPostCSSOptions);
|
||||
|
||||
/**
|
||||
* @const {object|boolean} developmentStylesArgs - The predefined `compileStyles()` options for development.
|
||||
* @const {object|boolean} productionStylesArgs - The predefined `compileStyles()` options for production.
|
||||
*/
|
||||
export const developmentStylesArgs = [
|
||||
developmentSassOptions,
|
||||
developmentPostCSSOptions,
|
||||
];
|
||||
export const productionStylesArgs = [
|
||||
productionSassOptions,
|
||||
productionPostCSSOptions,
|
||||
];
|
||||
|
||||
/**
|
||||
* Compiles and minifies main Sass files to CSS.
|
||||
*
|
||||
* @todo Add deep merge of `postcssOptions` to better support customization
|
||||
* of default processor options.
|
||||
*
|
||||
* @async
|
||||
* @param {object} [sassOptions=null] - Customize the Sass render API options.
|
||||
* If `null`, default production options are used.
|
||||
* @param {object|boolean} [postcssOptions=null] - Customize the PostCSS processor API options.
|
||||
* If `null`, default production options are used.
|
||||
* If `false`, PostCSS processing will be ignored.
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default async function compileStyles() {
|
||||
export default async function compileStyles(sassOptions = null, postcssOptions = null) {
|
||||
if (sassOptions == null) {
|
||||
sassOptions = productionSassOptions;
|
||||
} else if (
|
||||
sassOptions !== developmentSassOptions &&
|
||||
sassOptions !== productionSassOptions
|
||||
) {
|
||||
sassOptions = Object.assign({}, defaultSassOptions, sassOptions);
|
||||
}
|
||||
|
||||
if (postcss) {
|
||||
if (postcssOptions == null) {
|
||||
postcssOptions = productionPostCSSOptions;
|
||||
} else if (
|
||||
postcssOptions !== false &&
|
||||
postcssOptions !== developmentPostCSSOptions &&
|
||||
postcssOptions !== productionPostCSSOptions
|
||||
) {
|
||||
postcssOptions = Object.assign({}, defaultPostCSSOptions, postcssOptions);
|
||||
}
|
||||
}
|
||||
|
||||
loconfig.tasks.styles.forEach(async ({
|
||||
infile,
|
||||
outfile
|
||||
outfile,
|
||||
label = null
|
||||
}) => {
|
||||
const name = basename((outfile || 'undefined'), '.css');
|
||||
const filestem = basename((outfile || 'undefined'), '.css');
|
||||
|
||||
const timeLabel = `${name}.css compiled in`;
|
||||
const timeLabel = `${label || `${filestem}.css`} compiled in`;
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
infile = template(infile);
|
||||
outfile = template(outfile);
|
||||
infile = resolve(infile);
|
||||
outfile = resolve(outfile);
|
||||
|
||||
let result = await sassRender({
|
||||
let result = await sassRender(Object.assign({}, sassOptions, {
|
||||
file: infile,
|
||||
omitSourceMapUrl: true,
|
||||
outFile: outfile,
|
||||
outputStyle: 'compressed',
|
||||
sourceMap: true,
|
||||
sourceMapContents: true
|
||||
});
|
||||
}));
|
||||
|
||||
if (postcss) {
|
||||
result = await postcss.process(result.css, {
|
||||
from: outfile,
|
||||
to: outfile,
|
||||
map: {
|
||||
annotation: false,
|
||||
inline: false,
|
||||
sourcesContent: true
|
||||
}
|
||||
});
|
||||
if (postcss && postcssOptions) {
|
||||
if (typeof postcssProcessor === 'undefined') {
|
||||
postcssProcessor = createPostCSSProcessor(
|
||||
postcssPluginsMap,
|
||||
postcssOptions
|
||||
);
|
||||
}
|
||||
|
||||
result = await postcssProcessor.process(
|
||||
result.css,
|
||||
Object.assign({}, postcssOptions.processor, {
|
||||
from: outfile,
|
||||
to: outfile,
|
||||
})
|
||||
);
|
||||
|
||||
if (result.warnings) {
|
||||
const warnings = result.warnings();
|
||||
if (warnings.length) {
|
||||
message(`Error processing ${name}.css`, 'warning');
|
||||
message(`Error processing ${label || `${filestem}.css`}`, 'warning');
|
||||
warnings.forEach((warn) => {
|
||||
message(warn.toString());
|
||||
});
|
||||
@@ -65,17 +145,17 @@ export default async function compileStyles() {
|
||||
await writeFile(outfile, result.css);
|
||||
|
||||
if (result.css) {
|
||||
message(`${name}.css compiled`, 'success', timeLabel);
|
||||
message(`${label || `${filestem}.css`} compiled`, 'success', timeLabel);
|
||||
} else {
|
||||
message(`${name}.css is empty`, 'notice', timeLabel);
|
||||
message(`${label || `${filestem}.css`} is empty`, 'notice', timeLabel);
|
||||
}
|
||||
} catch (err) {
|
||||
message(`Error compiling ${name}.css`, 'error');
|
||||
message(`Error compiling ${label || `${filestem}.css`}`, 'error');
|
||||
message(err);
|
||||
|
||||
notification({
|
||||
title: `${name}.css save failed 🚨`,
|
||||
message: `Could not save stylesheet to ${name}.css`
|
||||
title: `${label || `${filestem}.css`} save failed 🚨`,
|
||||
message: `Could not save stylesheet to ${label || `${filestem}.css`}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,23 +163,52 @@ export default async function compileStyles() {
|
||||
try {
|
||||
await writeFile(outfile + '.map', result.map.toString());
|
||||
} catch (err) {
|
||||
message(`Error compiling ${name}.css.map`, 'error');
|
||||
message(`Error compiling ${label || `${filestem}.css.map`}`, 'error');
|
||||
message(err);
|
||||
|
||||
notification({
|
||||
title: `${name}.css.map save failed 🚨`,
|
||||
message: `Could not save sourcemap to ${name}.css.map`
|
||||
title: `${label || `${filestem}.css.map`} save failed 🚨`,
|
||||
message: `Could not save sourcemap to ${label || `${filestem}.css.map`}`
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
message(`Error compiling ${name}.scss`, 'error');
|
||||
message(`Error compiling ${label || `${filestem}.scss`}`, 'error');
|
||||
message(err.formatted || err);
|
||||
|
||||
notification({
|
||||
title: `${name}.scss compilation failed 🚨`,
|
||||
title: `${label || `${filestem}.scss`} compilation failed 🚨`,
|
||||
message: (err.formatted || `${err.name}: ${err.message}`)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,79 @@
|
||||
import loconfig from '../../loconfig.json';
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import template from '../utils/template.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import { basename } from 'node:path';
|
||||
import mixer from 'svg-mixer';
|
||||
|
||||
/**
|
||||
* @const {object} defaultMixerOptions - The default shared Mixer options.
|
||||
* @const {object} developmentMixerOptions - The predefined Mixer options for development.
|
||||
* @const {object} productionMixerOptions - The predefined Mixer options for production.
|
||||
*/
|
||||
export const defaultMixerOptions = {
|
||||
spriteConfig: {
|
||||
usages: false,
|
||||
},
|
||||
};
|
||||
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.
|
||||
*/
|
||||
export const developmentSVGsArgs = [
|
||||
developmentMixerOptions,
|
||||
];
|
||||
export const productionSVGsArgs = [
|
||||
productionMixerOptions,
|
||||
];
|
||||
|
||||
/**
|
||||
* Generates and transforms SVG spritesheets.
|
||||
*
|
||||
* @async
|
||||
* @param {object} [mixerOptions=null] - Customize the Mixer API options.
|
||||
* If `null`, default production options are used.
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default async function compileSVGs() {
|
||||
export default async function compileSVGs(mixerOptions = null) {
|
||||
if (mixerOptions == null) {
|
||||
mixerOptions = productionMixerOptions;
|
||||
} else if (
|
||||
mixerOptions !== developmentMixerOptions &&
|
||||
mixerOptions !== productionMixerOptions
|
||||
) {
|
||||
mixerOptions = Object.assign({}, defaultMixerOptions, mixerOptions);
|
||||
}
|
||||
|
||||
loconfig.tasks.svgs.forEach(async ({
|
||||
includes,
|
||||
outfile
|
||||
outfile,
|
||||
label = null
|
||||
}) => {
|
||||
const filename = basename(outfile || 'undefined');
|
||||
if (!label) {
|
||||
label = basename(outfile || 'undefined');
|
||||
}
|
||||
|
||||
const timeLabel = `${filename} compiled in`;
|
||||
const timeLabel = `${label} compiled in`;
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
includes = includes.map((path) => template(path));
|
||||
outfile = template(outfile);
|
||||
includes = resolve(includes);
|
||||
outfile = resolve(outfile);
|
||||
|
||||
const result = await mixer(includes, {
|
||||
spriteConfig: {
|
||||
usages: false
|
||||
}
|
||||
});
|
||||
const result = await mixer(includes, mixerOptions);
|
||||
|
||||
await result.write(outfile);
|
||||
|
||||
message(`${filename} compiled`, 'success', timeLabel);
|
||||
message(`${label} compiled`, 'success', timeLabel);
|
||||
} catch (err) {
|
||||
message(`Error compiling ${filename}`, 'error');
|
||||
message(`Error compiling ${label}`, 'error');
|
||||
message(err);
|
||||
|
||||
notification({
|
||||
title: `${filename} compilation failed 🚨`,
|
||||
title: `${label} compilation failed 🚨`,
|
||||
message: `${err.name}: ${err.message}`
|
||||
});
|
||||
}
|
||||
|
||||
64
build/utils/config.js
Normal file
64
build/utils/config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @file Provides simple user configuration options.
|
||||
*/
|
||||
|
||||
import loconfig from '../../loconfig.json';
|
||||
|
||||
let usrconfig;
|
||||
|
||||
try {
|
||||
usrconfig = await import('../../loconfig.local.json');
|
||||
usrconfig = usrconfig.default;
|
||||
|
||||
merge(loconfig, usrconfig);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default loconfig;
|
||||
|
||||
/**
|
||||
* Creates a new object with all nested object properties
|
||||
* merged into it recursively.
|
||||
*
|
||||
* @param {object} target - The target object.
|
||||
* @param {object[]} ...sources - The source object(s).
|
||||
* @throws {TypeError} If the target and source are the same.
|
||||
* @return {object} Returns the `target` object.
|
||||
*/
|
||||
export function merge(target, ...sources) {
|
||||
for (const source of sources) {
|
||||
if (target === source) {
|
||||
throw new TypeError(
|
||||
'Cannot merge, target and source are the same'
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
if (source[key] != null) {
|
||||
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
|
||||
merge(target[key], source[key]);
|
||||
continue;
|
||||
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
|
||||
target[key] = target[key].concat(source[key]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the passed value is an `Object`.
|
||||
*
|
||||
* @param {*} value - The value to be checked.
|
||||
* @return {boolean} Returns `true` if the value is an `Object`,
|
||||
* otherwise `false`.
|
||||
*/
|
||||
function isObjectLike(value) {
|
||||
return (value != null && typeof value === 'object');
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
* Note that options vary between libraries.
|
||||
*
|
||||
* Candidates:
|
||||
*
|
||||
* - {@link https://npmjs.com/package/tiny-glob tiny-glob}
|
||||
* - {@link https://npmjs.com/package/globby}
|
||||
* - {@link https://npmjs.com/package/globby globby}
|
||||
* - {@link https://npmjs.com/package/fast-glob fast-glob}
|
||||
* - {@link https://npmjs.com/package/glob glob}
|
||||
*/
|
||||
@@ -22,7 +23,15 @@ const candidates = [
|
||||
'glob',
|
||||
];
|
||||
|
||||
export default await importGlob();
|
||||
let glob;
|
||||
|
||||
try {
|
||||
glob = await importGlob();
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default glob;
|
||||
|
||||
/**
|
||||
* Imports the first available glob function.
|
||||
@@ -63,7 +72,7 @@ async function importGlob() {
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`No glob library was found, expected one of: ${modules.join(', ')}`
|
||||
`No glob library was found, expected one of: ${candidates.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file Provides a decorator for cross-platform notification.
|
||||
*/
|
||||
|
||||
import notifier from 'node-notifier';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
/**
|
||||
* @file If available, returns the PostCSS processor with any plugins.
|
||||
* @file If available, returns the PostCSS Processor creator and
|
||||
* any the Autoprefixer PostCSS plugin.
|
||||
*/
|
||||
|
||||
try {
|
||||
var { default: postcss } = await import('postcss');
|
||||
let { default: autoprefixer } = await import('autoprefixer');
|
||||
let postcss, autoprefixer;
|
||||
|
||||
postcss = postcss([ autoprefixer ]);
|
||||
try {
|
||||
postcss = await import('postcss');
|
||||
postcss = postcss.default;
|
||||
|
||||
autoprefixer = await import('autoprefixer');
|
||||
autoprefixer = autoprefixer.default;
|
||||
} catch (err) {
|
||||
postcss = null;
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default postcss;
|
||||
export const pluginsList = [
|
||||
autoprefixer,
|
||||
];
|
||||
export const pluginsMap = {
|
||||
'autoprefixer': autoprefixer,
|
||||
};
|
||||
export {
|
||||
autoprefixer
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @file Provides simple template tags.
|
||||
*/
|
||||
|
||||
import loconfig from '../../loconfig.json';
|
||||
import loconfig from './config.js';
|
||||
|
||||
const templateData = flatten({
|
||||
paths: loconfig.paths
|
||||
@@ -14,17 +14,53 @@ const templateData = flatten({
|
||||
* If replacement pairs contain a mix of substrings, regular expressions,
|
||||
* and functions, regular expressions are executed last.
|
||||
*
|
||||
* @param {string} input - The string being searched and replaced on.
|
||||
* @param {object} data - An object in the form `{ 'from': 'to', … }`.
|
||||
* @param {*} input - The value being searched and replaced on.
|
||||
* If input is, or contains, a string, tags will be resolved.
|
||||
* If input is, or contains, an object, it is mutated directly.
|
||||
* If input is, or contains, an array, a shallow copy is returned.
|
||||
* Otherwise, the value is left intact.
|
||||
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
|
||||
* @return {*} Returns the transformed value.
|
||||
*/
|
||||
export default function resolve(input, data = templateData) {
|
||||
switch (typeof input) {
|
||||
case 'string': {
|
||||
return resolveValue(input, data);
|
||||
}
|
||||
|
||||
case 'object': {
|
||||
if (input == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((value) => resolve(value, data));
|
||||
} else {
|
||||
for (const key in input) {
|
||||
input[key] = resolve(input[key], data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all template tags in a string from a map of keys and values.
|
||||
*
|
||||
* If replacement pairs contain a mix of substrings, regular expressions,
|
||||
* and functions, regular expressions are executed last.
|
||||
*
|
||||
* @param {string} input - The string being searched and replaced on.
|
||||
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
|
||||
* @return {string} Returns the translated string.
|
||||
*/
|
||||
export default function template(input, data) {
|
||||
export function resolveValue(input, data = templateData) {
|
||||
const tags = [];
|
||||
|
||||
if (data) {
|
||||
if (data !== templateData) {
|
||||
data = flatten(data);
|
||||
} else {
|
||||
data = templateData;
|
||||
}
|
||||
|
||||
for (let tag in data) {
|
||||
@@ -55,7 +91,7 @@ export default function template(input, data) {
|
||||
|
||||
return '';
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object with all nested object properties
|
||||
|
||||
255
build/watch.js
255
build/watch.js
@@ -1,82 +1,195 @@
|
||||
import loconfig from '../loconfig.json';
|
||||
import concatFiles from './tasks/concats.js';
|
||||
import compileScripts from './tasks/scripts.js';
|
||||
import compileStyles from './tasks/styles.js' ;
|
||||
import compileSVGs from './tasks/svgs.js';
|
||||
import template from './utils/template.js';
|
||||
import server from 'browser-sync';
|
||||
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 browserSync from 'browser-sync';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const { paths, tasks } = loconfig;
|
||||
|
||||
const serverConfig = {
|
||||
open: false,
|
||||
notify: false
|
||||
};
|
||||
|
||||
if (typeof paths.url === 'string' && paths.url.length > 0) {
|
||||
// Use proxy
|
||||
serverConfig.proxy = paths.url;
|
||||
} else {
|
||||
// Use base directory
|
||||
serverConfig.server = {
|
||||
baseDir: paths.dest
|
||||
};
|
||||
}
|
||||
|
||||
// Start the Browsersync server
|
||||
server.init(serverConfig);
|
||||
// Match a URL protocol.
|
||||
const regexUrlStartsWithProtocol = /^[a-z0-9\-]:\/\//i;
|
||||
|
||||
// Build scripts, compile styles, concat files,
|
||||
// and generate spritesheets on first hit
|
||||
concatFiles();
|
||||
compileScripts();
|
||||
compileStyles();
|
||||
compileSVGs();
|
||||
concatFiles(...developmentConcatFilesArgs);
|
||||
compileScripts(...developmentScriptsArgs);
|
||||
compileStyles(...developmentStylesArgs);
|
||||
compileSVGs(...developmentSVGsArgs);
|
||||
|
||||
// and call any methods on it.
|
||||
server.watch(
|
||||
[
|
||||
paths.views.src,
|
||||
join(paths.scripts.dest, '*.js'),
|
||||
join(paths.styles.dest, '*.css'),
|
||||
join(paths.svgs.dest, '*.svg'),
|
||||
]
|
||||
).on('change', server.reload);
|
||||
// Create a new BrowserSync instance
|
||||
const server = browserSync.create();
|
||||
|
||||
// Watch scripts
|
||||
server.watch(
|
||||
[
|
||||
join(paths.scripts.src, '**/*.js'),
|
||||
]
|
||||
).on('change', () => {
|
||||
compileScripts();
|
||||
// Start the BrowserSync server
|
||||
server.init(createServerOptions(loconfig), (err) => {
|
||||
if (err) {
|
||||
message('Error starting development server', 'error');
|
||||
message(err);
|
||||
|
||||
notification({
|
||||
title: 'Development server failed',
|
||||
message: `${err.name}: ${err.message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Watch concats
|
||||
server.watch(
|
||||
tasks.concats.reduce(
|
||||
(patterns, { includes }) => patterns.concat(includes),
|
||||
[]
|
||||
).map((path) => template(path))
|
||||
).on('change', () => {
|
||||
concatFiles();
|
||||
});
|
||||
configureServer(server, loconfig);
|
||||
|
||||
// Watch styles
|
||||
server.watch(
|
||||
[
|
||||
join(paths.styles.src, '**/*.scss'),
|
||||
]
|
||||
).on('change', () => {
|
||||
compileStyles();
|
||||
});
|
||||
/**
|
||||
* Configures the BrowserSync options.
|
||||
*
|
||||
* @param {BrowserSync} server - The BrowserSync API.
|
||||
* @param {object} loconfig - The project configset.
|
||||
* @param {object} loconfig.paths - The paths options.
|
||||
* @param {object} loconfig.tasks - The tasks options.
|
||||
* @return {void}
|
||||
*/
|
||||
function configureServer(server, { paths, tasks }) {
|
||||
const views = createViewsArray(paths.views);
|
||||
|
||||
// Watch svgs
|
||||
server.watch(
|
||||
[
|
||||
join(paths.svgs.src, '*.svg'),
|
||||
]
|
||||
).on('change', () => {
|
||||
compileSVGs();
|
||||
});
|
||||
// Reload on any changes to views or processed files
|
||||
server.watch(
|
||||
[
|
||||
...views,
|
||||
join(paths.scripts.dest, '*.js'),
|
||||
join(paths.styles.dest, '*.css'),
|
||||
join(paths.svgs.dest, '*.svg'),
|
||||
]
|
||||
).on('change', server.reload);
|
||||
|
||||
// Watch source scripts
|
||||
server.watch(
|
||||
[
|
||||
join(paths.scripts.src, '**/*.js'),
|
||||
]
|
||||
).on('change', () => {
|
||||
compileScripts(...developmentScriptsArgs);
|
||||
});
|
||||
|
||||
// Watch source concats
|
||||
server.watch(
|
||||
resolve(
|
||||
tasks.concats.reduce(
|
||||
(patterns, { includes }) => patterns.concat(includes),
|
||||
[]
|
||||
)
|
||||
)
|
||||
).on('change', () => {
|
||||
concatFiles(...developmentConcatFilesArgs);
|
||||
});
|
||||
|
||||
// Watch source styles
|
||||
server.watch(
|
||||
[
|
||||
join(paths.styles.src, '**/*.scss'),
|
||||
]
|
||||
).on('change', () => {
|
||||
compileStyles(...developmentStylesArgs);
|
||||
});
|
||||
|
||||
// Watch source SVGs
|
||||
server.watch(
|
||||
[
|
||||
join(paths.svgs.src, '*.svg'),
|
||||
]
|
||||
).on('change', () => {
|
||||
compileSVGs(...developmentSVGsArgs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object with all the BrowserSync options.
|
||||
*
|
||||
* @param {object} loconfig - The project configset.
|
||||
* @param {object} loconfig.paths - The paths options.
|
||||
* @param {object} loconfig.server - The server options.
|
||||
* @return {object} Returns the server options.
|
||||
*/
|
||||
function createServerOptions({
|
||||
paths,
|
||||
server: options
|
||||
}) {
|
||||
const config = {
|
||||
open: false,
|
||||
notify: false
|
||||
};
|
||||
|
||||
// Resolve the URL for the BrowserSync server
|
||||
if (isNonEmptyString(paths.url)) {
|
||||
// Use proxy
|
||||
config.proxy = paths.url;
|
||||
} else if (isNonEmptyString(paths.dest)) {
|
||||
// Use base directory
|
||||
config.server = {
|
||||
baseDir: paths.dest
|
||||
};
|
||||
}
|
||||
|
||||
merge(config, resolve(options));
|
||||
|
||||
// If HTTPS is enabled, prepend `https://` to proxy URL
|
||||
if (options?.https) {
|
||||
if (isNonEmptyString(config.proxy?.target)) {
|
||||
config.proxy.target = prependSchemeToUrl(config.proxy.target, 'https');
|
||||
} else if (isNonEmptyString(config.proxy)) {
|
||||
config.proxy = prependSchemeToUrl(config.proxy, 'https');
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new array (shallow-copied) from the views configset.
|
||||
*
|
||||
* @param {*} views - The views configset.
|
||||
* @throws {TypeError} If views is invalid.
|
||||
* @return {array} Returns the views array.
|
||||
*/
|
||||
function createViewsArray(views) {
|
||||
if (Array.isArray(views)) {
|
||||
return Array.from(views);
|
||||
}
|
||||
|
||||
switch (typeof views) {
|
||||
case 'string':
|
||||
return [ views ];
|
||||
|
||||
case 'object':
|
||||
if (views != null) {
|
||||
return Object.values(views);
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
'Expected \'views\' to be a string, array, or object'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the scheme to the URL.
|
||||
*
|
||||
* @param {string} url - The URL to mutate.
|
||||
* @param {string} [scheme] - The URL scheme to prepend.
|
||||
* @return {string} Returns the mutated URL.
|
||||
*/
|
||||
function prependSchemeToUrl(url, scheme = 'http') {
|
||||
if (regexUrlStartsWithProtocol.test(url)) {
|
||||
return url.replace(regexUrlStartsWithProtocol, `${scheme}://`);
|
||||
}
|
||||
|
||||
return `${scheme}://${url}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the passed value is a string with at least one character.
|
||||
*
|
||||
* @param {*} value - The value to be checked.
|
||||
* @return {boolean} Returns `true` if the value is a non-empty string,
|
||||
* otherwise `false`.
|
||||
*/
|
||||
function isNonEmptyString(value) {
|
||||
return (typeof value === 'string' && value.length > 0);
|
||||
}
|
||||
|
||||
344
docs/development.md
Normal file
344
docs/development.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Development
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Usage](#usage)
|
||||
* [Configuration](#configuration)
|
||||
* [Environment Configuration](#environment-configuration)
|
||||
* [Development Configuration](#development-configuration)
|
||||
* [`paths` option](#paths-option)
|
||||
* [`paths.url` option](#pathsurl-option)
|
||||
* [`paths.dest` option](#pathsdest-option)
|
||||
* [`tasks` option](#tasks-option)
|
||||
* [`server` option](#server-option)
|
||||
* [Tasks](#tasks)
|
||||
* [`concats`](#concats)
|
||||
* [`scripts`](#scripts)
|
||||
* [`styles`](#styles)
|
||||
* [`svgs`](#svgs)
|
||||
|
||||
---
|
||||
|
||||
The boilerplate provides a custom, easily configured, and very simple,
|
||||
task runner for [Node] to process assets and test quickly in browsers.
|
||||
|
||||
Learn more about the boilerplate's [tasks](#tasks) below.
|
||||
|
||||
## Installation
|
||||
|
||||
Make sure you have the following installed:
|
||||
|
||||
* [Node] — at least 14.17, the latest LTS is recommended.
|
||||
* [NPM] — at least 6.0, the latest LTS is recommended.
|
||||
|
||||
```sh
|
||||
# Switch to recommended Node version from .nvmrc
|
||||
nvm use
|
||||
|
||||
# Install dependencies from package.json
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
# Start development server, watch for changes, and compile assets
|
||||
npm start
|
||||
|
||||
# Compile and minify assets
|
||||
npm run build
|
||||
```
|
||||
|
||||
See [`build.js`](../build/build.js) and [`watch.js`](../build/watch.js)
|
||||
for details.
|
||||
|
||||
## Configuration
|
||||
|
||||
For development, most configuration values for processing front-end assets
|
||||
are defined in the [`loconfig.json`](../loconfig.json) file that exists at
|
||||
the root directory of your project.
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
If any configuration options vary depending on whether your project is
|
||||
running on your computer, a collaborator's computer, or on a web server,
|
||||
these values should be stored in a `loconfig.local.json` file.
|
||||
|
||||
In fresh copy of the boilerplate, the root directory of your project
|
||||
will contain a [`loconfig.example.json`](../loconfig.example.json) file.
|
||||
|
||||
> 💡 The boilerplate's default example customizes the development server
|
||||
> to use a custom SSL certificate.
|
||||
|
||||
That file can be copied to `loconfig.local.json` and customized to suit
|
||||
your local environment.
|
||||
|
||||
Your `loconfig.local.json` _should not_ be committed to your project's
|
||||
source control.
|
||||
|
||||
> 💡 If you are developing with a team, you may wish to continue
|
||||
> including a `loconfig.example.json` file with your project.
|
||||
|
||||
### Development Configuration
|
||||
|
||||
The boilerplate provides a few configuration settings to control the
|
||||
behaviour for processing front-end assets.
|
||||
|
||||
#### `paths` option
|
||||
|
||||
The `paths` option defines URIs and file paths.
|
||||
|
||||
It is primarily used for template tags to reference any configuration
|
||||
properties to reduce repetition.
|
||||
|
||||
Template tags are specified using `{% %}` delimiters. They will be
|
||||
automatically expanded when tasks process paths.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"paths": {
|
||||
"styles": {
|
||||
"src": "./assets/styles",
|
||||
"dest": "./www/assets/styles"
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"styles": [
|
||||
{
|
||||
"infile": "{% paths.styles.src %}/main.scss", // → ./assets/styles/main.scss
|
||||
"outfile": "{% paths.styles.dest %}/main.css" // → ./www/assets/styles/main.css
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `paths.url` option
|
||||
|
||||
The `paths.url` option defines the base URI of the project.
|
||||
|
||||
By default, it is used by the development server as a proxy
|
||||
for an existing virtual host.
|
||||
|
||||
```json
|
||||
{
|
||||
"paths": {
|
||||
"url": "locomotive-boilerplate.test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `paths.dest` option
|
||||
|
||||
The `paths.dest` option defines the public web directory of the project.
|
||||
|
||||
By default, it is used by the development server as the base directory
|
||||
to serve the website from if a proxy URI is not provided.
|
||||
|
||||
```json
|
||||
{
|
||||
"paths": {
|
||||
"dest": "./www"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `tasks` option
|
||||
|
||||
Which assets and how they should be processed can be configured via
|
||||
the `tasks` option:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"scripts": [
|
||||
{
|
||||
"includes": [
|
||||
"./assets/scripts/app.js"
|
||||
],
|
||||
"outfile": "./www/assets/scripts/app.js"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
{
|
||||
"infile": "./assets/styles/main.scss",
|
||||
"outfile": "./www/assets/styles/main.css"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [tasks](#tasks) section, below, for details.
|
||||
|
||||
#### `server` option
|
||||
|
||||
The development server (BrowserSync) can be configured via
|
||||
the `server` option:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"open": true,
|
||||
"https": {
|
||||
"key": "~/.config/valet/Certificates/{% paths.url %}.key",
|
||||
"cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Visit [BrowserSync's documentation](https://browsersync.io/docs/options)
|
||||
for all options.
|
||||
|
||||
## Tasks
|
||||
|
||||
The boilerplate provides a handful of tasks for handling
|
||||
the most commonly processed assets.
|
||||
|
||||
### `concats`
|
||||
|
||||
A wrapper around [concat] (with optional support for globbing) for concatenating multiple files.
|
||||
|
||||
By default, [tiny-glob] is installed with the boilerplate.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"concats": [
|
||||
{
|
||||
"label": "Application Vendors",
|
||||
"includes": [
|
||||
"{% paths.scripts.src %}/vendors/*.js",
|
||||
"node_modules/focus-visible/dist/focus-visible.min.js",
|
||||
"node_modules/vue/dist/vue.min.js",
|
||||
"node_modules/vuelidate/dist/vuelidate.min.js",
|
||||
"node_modules/vuelidate/dist/validators.min.js"
|
||||
],
|
||||
"outfile": "{% paths.scripts.dest %}/app/vendors.js"
|
||||
},
|
||||
{
|
||||
"label": "Public Site Vendors",
|
||||
"includes": [
|
||||
"{% paths.scripts.src %}/vendors/*.js",
|
||||
"node_modules/focus-visible/dist/focus-visible.min.js"
|
||||
],
|
||||
"outfile": "{% paths.scripts.dest %}/site/vendors.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See [`concats.js`](../build/tasks/concats.js) for details.
|
||||
|
||||
### `scripts`
|
||||
|
||||
A wrapper around [esbuild] for bundling and minifying modern JS/ES modules.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": [
|
||||
{
|
||||
"label": "Application Dashboard JS",
|
||||
"includes": [
|
||||
"{% paths.scripts.src %}/app/dashboard.js"
|
||||
],
|
||||
"outfile": "{% paths.scripts.dest %}/app/dashboard.js"
|
||||
},
|
||||
{
|
||||
"label": "Public Site JS",
|
||||
"includes": [
|
||||
"{% paths.scripts.src %}/site/main.js"
|
||||
],
|
||||
"outfile": "{% paths.scripts.dest %}/site/main.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See [`scripts.js`](../build/tasks/scripts.js) for details.
|
||||
|
||||
### `styles`
|
||||
|
||||
A wrapper around [node-sass] (and optionally [Autoprefixer] via [PostCSS])
|
||||
for compiling and minifying Sass into CSS.
|
||||
|
||||
By default, [PostCSS] and [Autoprefixer] are installed with the boilerplate.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"styles": [
|
||||
{
|
||||
"label": "Text Editor CSS",
|
||||
"infile": "{% paths.styles.src %}/app/editor.scss",
|
||||
"outfile": "{% paths.styles.dest %}/app/editor.css"
|
||||
},
|
||||
{
|
||||
"label": "Application Dashboard CSS",
|
||||
"infile": "{% paths.styles.src %}/app/dashboard.scss",
|
||||
"outfile": "{% paths.styles.dest %}/app/dashboard.css"
|
||||
},
|
||||
{
|
||||
"label": "Public Site Critical CSS",
|
||||
"infile": "{% paths.styles.src %}/site/critical.scss",
|
||||
"outfile": "{% paths.styles.dest %}/site/critical.css"
|
||||
},
|
||||
{
|
||||
"label": "Public Site CSS",
|
||||
"infile": "{% paths.styles.src %}/site/main.scss",
|
||||
"outfile": "{% paths.styles.dest %}/site/main.css"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See [`styles.js`](../build/tasks/styles.js) for details.
|
||||
|
||||
### `svgs`
|
||||
|
||||
A wrapper around [SVG Mixer] for transforming and minifying SVG files
|
||||
and generating spritesheets.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"svgs": [
|
||||
{
|
||||
"label": "Application Spritesheet",
|
||||
"includes": [
|
||||
"{% paths.images.src %}/app/*.svg"
|
||||
],
|
||||
"outfile": "{% paths.svgs.dest %}/app/sprite.svg"
|
||||
},
|
||||
{
|
||||
"label": "Public Site Spritesheet",
|
||||
"includes": [
|
||||
"{% paths.images.src %}/site/*.svg"
|
||||
],
|
||||
"outfile": "{% paths.svgs.dest %}/site/sprite.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See [`svgs.js`](../build/tasks/svgs.js) for details.
|
||||
|
||||
[Autoprefixer]: https://npmjs.com/package/autoprefixer
|
||||
[BrowserSync]: https://npmjs.com/package/browser-sync
|
||||
[concat]: https://npmjs.com/package/concat
|
||||
[esbuild]: https://npmjs.com/package/esbuild
|
||||
[fast-glob]: https://npmjs.com/package/fast-glob
|
||||
[glob]: https://npmjs.com/package/glob
|
||||
[globby]: https://npmjs.com/package/globby
|
||||
[Node]: https://nodejs.org/
|
||||
[node-sass]: https://npmjs.com/package/node-sass
|
||||
[NPM]: https://npmjs.com/
|
||||
[NVM]: https://github.com/nvm-sh/nvm
|
||||
[PostCSS]: https://npmjs.com/package/postcss
|
||||
[SVG Mixer]: https://npmjs.com/package/svg-mixer
|
||||
[tiny-glob]: https://npmjs.com/package/tiny-glob
|
||||
207
docs/technologies.md
Normal file
207
docs/technologies.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Technologies
|
||||
|
||||
* [Styles](#styles)
|
||||
* [CSS Architecture](#css-architecture)
|
||||
* [CSS Naming Convention](#css-naming-convention)
|
||||
* [CSS Namespacing](#css-namespacing)
|
||||
* [Example](#example-1)
|
||||
* [Scripts](#scripts)
|
||||
* [Example](#example-2)
|
||||
* [Page transitions](#page-transitions)
|
||||
* [Example](#example-3)
|
||||
* [Scroll detection](#scroll-detection)
|
||||
* [Example](#example-4)
|
||||
|
||||
## Styles
|
||||
|
||||
[SCSS][Sass] is a superset of CSS that adds many helpful features to improve
|
||||
and modularize our styles.
|
||||
|
||||
We use [node-sass] (LibSass) for processing and minifying SCSS into CSS.
|
||||
|
||||
We also use [PostCSS] and [Autoprefixer] to parse our CSS and add
|
||||
vendor prefixes for experimental features.
|
||||
|
||||
### CSS Architecture
|
||||
|
||||
The boilerplate's CSS architecture is based on [Inuit CSS][inuitcss] and [ITCSS].
|
||||
|
||||
* `settings`: Global variables, site-wide settings, config switches, etc.
|
||||
* `tools`: Site-wide mixins and functions.
|
||||
* `generic`: Low-specificity, far-reaching rulesets (e.g. resets).
|
||||
* `elements`: Unclassed HTML elements (e.g. `a {}`, `blockquote {}`, `address {}`).
|
||||
* `objects`: Objects, abstractions, and design patterns (e.g. `.o-layout {}`).
|
||||
* `components`: Discrete, complete chunks of UI (e.g. `.c-carousel {}`).
|
||||
* `utilities`: High-specificity, very explicit selectors. Overrides and helper
|
||||
classes (e.g. `.u-hidden {}`)
|
||||
|
||||
Learn more about [Inuit CSS](https://github.com/inuitcss/inuitcss#css-directory-structure).
|
||||
|
||||
### CSS Naming Convention
|
||||
|
||||
We use a simplified [BEM] (Block, Element, Modifier) syntax:
|
||||
|
||||
* `.block`
|
||||
* `.block_element`
|
||||
* `.-modifier`
|
||||
|
||||
### CSS Namespacing
|
||||
|
||||
We namespace our classes for more UI transparency:
|
||||
|
||||
* `o-`: Object that it may be used in any number of unrelated contexts to the one you can currently see it in. Making modifications to these types of class could potentially have knock-on effects in a lot of other unrelated places.
|
||||
* `c-`: Component is a concrete, implementation-specific piece of UI. All of the changes you make to its styles should be detectable in the context you’re currently looking at. Modifying these styles should be safe and have no side effects.
|
||||
* `u-`: Utility has a very specific role (often providing only one declaration) and should not be bound onto or changed. It can be reused and is not tied to any specific piece of UI.
|
||||
* `s-`: Scope creates a new styling context. Similar to a Theme, but not necessarily cosmetic, these should be used sparingly—they can be open to abuse and lead to poor CSS if not used wisely.
|
||||
* `is-`, `has-`: Is currently styled a certain way because of a state or condition. It tells us that the DOM currently has a temporary, optional, or short-lived style applied to it due to a certain state being invoked.
|
||||
|
||||
Learn about [namespacing](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/).
|
||||
|
||||
### Example \#1
|
||||
|
||||
```html
|
||||
<div class="c-block -large">
|
||||
<div class="c-block_layout o-layout">
|
||||
<div class="o-layout_item u-1/2@from-medium">
|
||||
<div class="c-block_heading o-h -medium">Heading</div>
|
||||
</div>
|
||||
<div class="o-layout_item u-1/2@from-medium">
|
||||
<a class="c-block_button o-button -outline" href="#">Button</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```scss
|
||||
.c-block {
|
||||
&.-large {
|
||||
padding: rem(60px);
|
||||
}
|
||||
}
|
||||
|
||||
.c-block_heading {
|
||||
@media (max-width: $to-medium) {
|
||||
.c-block.-large & {
|
||||
margin-bottom: rem(40px);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
We use [esbuild] for bundling and minifying JavaScript/ES modules.
|
||||
|
||||
[modularJS] is a small framework we use on top of ES modules.
|
||||
|
||||
* Automatically init visible modules.
|
||||
* Easily call other modules methods.
|
||||
* Quickly set scoped events with delegation.
|
||||
* Simply select DOM elements scoped in their module.
|
||||
|
||||
[_source_](https://npmjs.com/package/modujs#why)
|
||||
|
||||
### Example \#2
|
||||
|
||||
```html
|
||||
<div data-module-example>
|
||||
<div data-example="main">
|
||||
<h2>Example</h2>
|
||||
</div>
|
||||
<button data-example="load">More</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import { module } from 'modujs';
|
||||
|
||||
export default class extends module {
|
||||
constructor(m) {
|
||||
super(m);
|
||||
|
||||
this.events = {
|
||||
click: {
|
||||
load: 'loadMore'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.$('main')[0].classList.add('is-loading');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about [modularJS].
|
||||
|
||||
## Page transitions
|
||||
|
||||
[modularLoad] is used for page transitions and lazy loading.
|
||||
|
||||
### Example \#3
|
||||
|
||||
```html
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/page" data-load="transitionName">Page</a>
|
||||
</nav>
|
||||
<div data-load-container>
|
||||
<img data-load-src="assets/images/hello.jpg">
|
||||
</div>
|
||||
```
|
||||
```js
|
||||
import modularLoad from 'modularload';
|
||||
|
||||
this.load = new modularLoad({
|
||||
enterDelay: 300,
|
||||
transitions: {
|
||||
transitionName: {
|
||||
enterDelay: 450
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Learn more about [modularLoad].
|
||||
|
||||
## Scroll detection
|
||||
|
||||
[Locomotive Scroll][locomotive-scroll] is used for elements in viewport
|
||||
detection and smooth scrolling with parallax.
|
||||
|
||||
### Example \#4
|
||||
|
||||
```html
|
||||
<div data-module-scroll>
|
||||
<div data-scroll>Trigger</div>
|
||||
<div data-scroll data-scroll-speed="1">Parallax</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import LocomotiveScroll from 'locomotive-scroll';
|
||||
|
||||
this.scroll = new LocomotiveScroll({
|
||||
el: this.el,
|
||||
smooth: true
|
||||
});
|
||||
````
|
||||
|
||||
Learn more about [Locomotive Scroll][locomotive-scroll].
|
||||
|
||||
[Autoprefixer]: https://npmjs.com/package/autoprefixer
|
||||
[BEM]: https://bem.info/
|
||||
[BrowserSync]: https://npmjs.com/package/browser-sync
|
||||
[esbuild]: https://npmjs.com/package/esbuild
|
||||
[inuitcss]: https://github.com/inuitcss/inuitcss
|
||||
[ITCSS]: https://itcss.io/
|
||||
[locomotive-scroll]: https://npmjs.com/package/locomotive-scroll
|
||||
[modularJS]: https://npmjs.com/package/modujs
|
||||
[modularLoad]: https://npmjs.com/package/modularload
|
||||
[node-sass]: https://npmjs.com/package/node-sass
|
||||
[PostCSS]: https://npmjs.com/package/postcss
|
||||
[Sass]: https://sass-lang.com/
|
||||
[svg-mixer]: https://npmjs.com/package/svg-mixer
|
||||
[Node]: https://nodejs.org/
|
||||
[NPM]: https://npmjs.com/
|
||||
[NVM]: https://github.com/nvm-sh/nvm
|
||||
8
loconfig.example.json
Executable file
8
loconfig.example.json
Executable file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"server": {
|
||||
"https": {
|
||||
"key": "~/.config/valet/Certificates/{% paths.url %}.key",
|
||||
"cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
|
||||
}
|
||||
}
|
||||
}
|
||||
4685
package-lock.json
generated
4685
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -14,21 +14,21 @@
|
||||
"build": "node --experimental-json-modules --no-warnings build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"locomotive-scroll": "^4.1.3",
|
||||
"locomotive-scroll": "^4.1.4",
|
||||
"modujs": "^1.4.2",
|
||||
"modularload": "^1.2.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
"svg4everybody": "^2.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.3.7",
|
||||
"browser-sync": "^2.26.13",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"browser-sync": "^2.27.9",
|
||||
"concat": "^1.0.3",
|
||||
"esbuild": "^0.13.4",
|
||||
"esbuild": "^0.14.27",
|
||||
"kleur": "^4.1.4",
|
||||
"node-notifier": "^10.0.0",
|
||||
"node-sass": "^6.0.1",
|
||||
"postcss": "^8.3.9",
|
||||
"node-notifier": "^10.0.1",
|
||||
"node-sass": "^7.0.1",
|
||||
"postcss": "^8.4.12",
|
||||
"svg-mixer": "^2.3.14",
|
||||
"tiny-glob": "^0.2.9"
|
||||
}
|
||||
|
||||
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
@@ -31,7 +31,7 @@
|
||||
|
||||
<main data-scroll-section>
|
||||
<div class="o-container">
|
||||
<h1 class="c-heading -h1">Page</h1>
|
||||
<h1 class="t-h1">Page</h1>
|
||||
|
||||
<div class="o-layout">
|
||||
<div class="o-layout_item u-1/2@from-small">
|
||||
|
||||
@@ -31,19 +31,19 @@
|
||||
|
||||
<main data-scroll-section>
|
||||
<div class="o-container">
|
||||
<h1 class="c-heading -h1">Images</h1>
|
||||
<h1 class="t-h1">Images</h1>
|
||||
|
||||
<section>
|
||||
<h2 class="c-heading -h2">Lazy load demo</h2>
|
||||
<h2 class="t-h2">Lazy load demo</h2>
|
||||
|
||||
<h3 class="c-heading -h3">Basic</h3>
|
||||
<h3 class="t-h3">Basic</h3>
|
||||
|
||||
<div style="width: 640px; max-width: 100%;">
|
||||
<div class="o-ratio u-4:3"><img data-load-src="http://picsum.photos/640/480?v=1" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /></div>
|
||||
<div class="o-ratio u-4:3"><img data-load-src="http://picsum.photos/640/480?v=2" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /></div>
|
||||
</div>
|
||||
|
||||
<h4 class="c-heading -h3">Using o-ratio & background-image</h3>
|
||||
<h4 class="t-h3">Using o-ratio & background-image</h3>
|
||||
|
||||
<div style="width: 480px; max-width: 100%;">
|
||||
<div class="o-ratio u-16:9" data-load-style="background-size: cover; background-position: center; background-image: url(http://picsum.photos/640/480?v=1);"></div>
|
||||
@@ -52,9 +52,9 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="c-heading -h3">Relative to scroll</h3>
|
||||
<h3 class="t-h3">Relative to scroll</h3>
|
||||
|
||||
<h4 class="c-heading -h3">Using o-ratio & img</h3>
|
||||
<h4 class="t-h3">Using o-ratio & img</h3>
|
||||
|
||||
<div style="width: 640px; max-width: 100%;">
|
||||
<div class="o-ratio u-4:3">
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="c-heading -h3">Using o-ratio & background-image</h3>
|
||||
<h4 class="t-h3">Using o-ratio & background-image</h3>
|
||||
|
||||
<div style="width: 480px; max-width: 100%;">
|
||||
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/640/480?v=1"></div>
|
||||
@@ -84,7 +84,7 @@
|
||||
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/640/480?v=5"></div>
|
||||
</div>
|
||||
|
||||
<h4 class="c-heading -h3">Using SVG viewport for ratio</h3>
|
||||
<h4 class="t-h3">Using SVG viewport for ratio</h3>
|
||||
|
||||
<div style="width: 480px; max-width: 100%;">
|
||||
<img
|
||||
|
||||
@@ -50,7 +50,17 @@
|
||||
|
||||
<main data-scroll-section>
|
||||
<div class="o-container">
|
||||
<h1 class="c-heading -h1">Hello</h1>
|
||||
<h1 class="t-h1">Hello</h1>
|
||||
<div class="t-wysiwyg">
|
||||
<h2>Crucifix de charrue de cibouleau.</h2>
|
||||
<p>Cossin de ciarge de sacristi de calvaire de <strong>bâtard de caltor</strong> de mosus d'étole de boswell de sacrament de sapristi de gériboire de Jésus de plâtre.</p>
|
||||
<ul>
|
||||
<li>Viande à chien de saint-cimonaque.</li>
|
||||
<li>Sacristi de calvince de charogne de saint-ciarge.</li>
|
||||
</ul>
|
||||
<h3>Calvaire de gériboire de saintes fesses.</h3>
|
||||
<p>Viande à chien de boswell de sacristi de patente à gosse de mangeux d'marde de <i>Jésus de plâtre de gériboire de purée de charrue</i> de christie de torrieux de cimonaque de saint-ciarge de bâtard de maudite marde de cochonnerie.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user