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