```
## Features
- **Lightweight** — Only 9.4kB gzipped
- **TypeScript First** — Fully typed
- **Built on Lenis** — Latest stable release with improved performance
- **Dual Intersection Observers** — Optimized detection for triggers vs. animations
- **Smart Touch Detection** — Parallax auto-disabled on mobile
- **Accessible** — Native scrollbar, keyboard navigation, proper ARIA support
## Demo
Check out the [examples and playground](https://scroll.locomotive.ca/docs/examples)
## Support
[GitHub Issues](https://github.com/locomotivemtl/locomotive-scroll/issues)
================================================
FILE: context7.json
================================================
{
"url": "https://context7.com/locomotivemtl/locomotive-scroll",
"public_key": "pk_fFYCHZCsmlAjABaNmHyFM",
"foldersInclude": ["packages/lib"],
"foldersExclude": ["packages/docs", "packages/demo", "packages/landing", "node_modules"],
"customRules": [
"locomotive-scroll v5 wraps Lenis for smooth scrolling - use lenisOptions for scroll physics (lerp, duration, easing)",
"Elements with data-scroll-speed, data-scroll-css-progress, or data-scroll-offset trigger RAF updates every frame - minimize these for performance",
"data-scroll-speed is relative to container size, not absolute pixels - speed: 0.5 means 50% of container height displacement",
"Parallax is auto-disabled on touch devices - add data-scroll-enable-touch-speed to force it on mobile",
"scrollPosition defines WHERE in viewport to trigger (start/middle/end), scrollOffset defines WHEN with a px/% offset - they are different",
"Elements visible at page load (in fold) have different progress mapping (0→1 vs -1→1) - use data-scroll-ignore-fold to disable",
"Dynamically added elements need locomotiveScroll.addScrollElements($container) - just adding HTML to DOM won't register them",
"Custom scroll containers need fixed height + overflow hidden/auto on wrapper, with content as direct child",
"Use data-scroll-repeat for elements that should re-trigger on each scroll in/out - default is fire once",
"Access Lenis instance via locomotiveScroll.lenisInstance for programmatic scrollTo, stop(), start()"
]
}
================================================
FILE: package.json
================================================
{
"private": true,
"name": "locomotive-scroll",
"description": "Monorepo for Locomotive Scroll",
"license": "MIT",
"homepage": "https://github.com/locomotivemtl/locomotive-scroll",
"repository": {
"type": "git",
"url": "https://github.com/locomotivemtl/locomotive-scroll.git"
},
"author": {
"name": "Locomotive",
"email": "info@locomotive.ca",
"homepage": "https://locomotive.ca"
},
"devDependencies": {
"turbo": "^2.7.4",
"typescript": "^4.8.3"
},
"scripts": {
"build": "turbo run build",
"build:vercel": "turbo run build --filter=./packages/lib --filter=./packages/demo --filter=./packages/docs",
"build:landing": "npm run build --workspace=packages/landing",
"dev": "npm run dev --workspace=packages/demo --if-present",
"publish": "npm run publish --workspace=packages/lib --if-present",
"publish:next": "npm run publish:next --workspace=packages/lib --if-present"
},
"workspaces": [
"packages/lib",
"packages/demo",
"packages/landing",
"packages/docs"
],
"version": "5.0.0-rc.1",
"packageManager": "npm@10.9.4"
}
================================================
FILE: packages/demo/.editorconfig
================================================
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{md,markdown}]
trim_trailing_whitespace = false
================================================
FILE: packages/demo/.gitignore
================================================
# build output
dist/
# SVG sprite
public/assets/images/sprite.svg
================================================
FILE: packages/demo/.nvmrc
================================================
v20.14
================================================
FILE: packages/demo/.prettierignore
================================================
node_modules/**
================================================
FILE: packages/demo/.prettierrc
================================================
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"semi": true,
"printWidth": 100,
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"]
}
================================================
FILE: packages/demo/.vscode/extensions.json
================================================
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
================================================
FILE: packages/demo/.vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
================================================
FILE: packages/demo/.vscode/settings.json
================================================
{
"css.customData": [".vscode/tailwind.json"],
"scss.lint.unknownAtRules": "ignore",
}
================================================
FILE: packages/demo/.vscode/tailwind.json
================================================
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}
================================================
FILE: packages/demo/LICENSE
================================================
The MIT License (MIT)
Copyright (c) Locomotive, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: packages/demo/README.md
================================================
Locomotive Astro Boilerplate
Front-end Astro boilerplate for projects by Locomotive.
## Features
* Uses [Astro] for all-in-one web framework.
* Uses [Sass] for a feature rich superset of CSS.
* Uses [Tailwind CSS] for a sane and scalable CSS architecture.
* Uses [Locomotive Scroll] for smooth scrolling with parallax effects.
* Uses [Swup] for versatile and extensible page transition.
* Uses [Prettier] for a formatted and easy to maintain codebase.
## Getting started
Make sure you have the following installed:
* [Node] — at least 20.14, the latest LTS is recommended.
* [NPM] — at least 8.0, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
```sh
# Clone the repository.
git clone https://github.com/locomotivemtl/astro-boilerplate.git my-new-project
# Enter the newly-cloned directory.
cd my-new-project
```
## Installation
```sh
# Switch to recommended Node version from .nvmrc
nvm use
# Install dependencies from package.json
npm install
```
## Development
```sh
# Start development server, watch for changes, and compile assets
npm start
# Compile and minify assets
npm run build
```
## Project Structure
Inside of your project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card/
│ │ ├── Card.astro
│ │ └── Card.scss
│ ├── layouts/
│ │ └── Layout.astro
│ ├── pages/
│ │ └── index.astro
│ ├── styles/
│ │ └── main.scss
│ └── scripts/
│ ├── components/
│ ├── utils/
│ ├── app.ts
│ └── config.ts
└── package.json
```
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
| `npm run format` | Format files using prettier |
## Documentation
* [Astro]
* [Locomotive Scroll]
* [Tailwind CSS]
* [Swup]
* [Prettier]
[Astro]: https://docs.astro.build/en/getting-started/
[Tailwind CSS]: https://tailwindcss.com/docs/installation
[Locomotive Scroll]: https://scroll.locomotive.ca/docs
[Sass]: https://sass-lang.com/
[Swup]: https://swup.js.org/getting-started/
[Node]: https://nodejs.org/
[NPM]: https://npmjs.com/
[NVM]: https://github.com/nvm-sh/nvm
[Prettier]: https://prettier.io/
================================================
FILE: packages/demo/astro.config.ts
================================================
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import tailwindConfig from './tailwind.config';
import postcssTailwindShortcuts from '@locomotivemtl/postcss-tailwind-shortcuts';
// https://astro.build/config
export default defineConfig({
site: 'https://scroll.locomotive.ca/demo',
base: '/demo',
outDir: '../../www/demo',
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "sass:math";
@use "sass:list";
@use "@styles/tools/maths" as *;
@use "@styles/tools/functions" as *;
`
}
},
postcss: {
plugins: [
postcssTailwindShortcuts(tailwindConfig.theme),
],
}
}
},
integrations: [
tailwind({
applyBaseStyles: false,
}),
],
devToolbar: {
enabled: false
},
image: {
domains: ['locomotive.ca'],
remotePatterns: [{ protocol: 'https' }],
}
});
================================================
FILE: packages/demo/package.json
================================================
{
"private": true,
"name": "@locomotivemtl/astro-boilerplate",
"title": "Locomotive Boilerplate",
"type": "module",
"version": "0.1.0",
"engines": {
"node": "20.x",
"npm": ">=8.0"
},
"repository": {
"type": "git",
"url": "https://github.com/locomotivemtl/astro-boilerplate.git"
},
"author": {
"name": "Locomotive",
"email": "info@locomotive.ca",
"homepage": "https://locomotive.ca"
},
"scripts": {
"dev": "astro dev --host",
"start": "astro dev --host",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"format": "prettier --write \"src/**/*.{astro,js,ts,css,scss}\""
},
"dependencies": {
"@astrojs/check": "^0.9.2",
"@astrojs/tailwind": "^5.1.0",
"astro": "^4.13.4",
"astro-seo": "^0.8.4",
"locomotive-scroll": "*",
"nanostores": "^0.10.3",
"sass": "^1.77.4",
"swup": "^4.7.0",
"ts-debounce": "^4.0.0"
},
"devDependencies": {
"@locomotivemtl/postcss-tailwind-shortcuts": "^1.0.0",
"prettier": "^3.3.1",
"prettier-plugin-astro": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.6.1"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.5"
}
}
================================================
FILE: packages/demo/public/fonts/.gitkeep
================================================
================================================
FILE: packages/demo/src/components/ScrollToggler/ScrollToggler.astro
================================================
================================================
FILE: packages/demo/src/env.d.ts
================================================
///
================================================
FILE: packages/demo/src/layouts/Layout.astro
================================================
---
import '@styles/main.scss';
import { SEO } from 'astro-seo';
interface Props {
title: string;
seo?: Seo;
scrollOrientation?: 'vertical' | 'horizontal';
}
const { title, scrollOrientation = 'vertical' } = Astro.props;
const FONTS: string[] = [
// 'WebfontRegular.woff2',
// 'WebfontBold.woff2',
]
---
{FONTS.map(font =>
)}
================================================
FILE: packages/demo/src/pages/horizontal.astro
================================================
---
import Layout from '@layouts/Layout.astro';
---
Locomotive Scroll
V
.
0
X
data-scroll-speed
-.1
0
.5
-.2
data-scroll-class
c-scroll-opacity
c-scroll-rotate
data-scroll-repeat
c-scroll-opacity
REPEAT
data-scroll-css-progress
css progress variable
data-scroll-event-progress: Custom Event
data-scroll-position
Position Enter: start
Position Leave: end
Position Enter: middle
Position Leave: middle
Position Enter: end
Position Leave: start
data-scroll-call: Custom Event
Lenis scroll calback: direction
================================================
FILE: packages/demo/src/pages/index.astro
================================================
---
import Layout from "../layouts/Layout.astro"
import ScrollToggler from '@components/ScrollToggler/ScrollToggler.astro';
---
Locomotive Scroll
V
.
5
data-scroll-speed
-.1
0
.5
-.2
data-scroll-class
c-scroll-opacity
c-scroll-rotate
data-scroll-repeat
c-scroll-opacity
REPEAT
data-scroll-css-progress
css progress variable
data-scroll-event-progress: Custom Event
data-scroll-position
Position Enter: start
Position Leave: end
Position Enter: middle
Position Leave: middle
Position Enter: end
Position Leave: start
data-scroll-call: Custom Event
Lenis scroll calback: direction
================================================
FILE: packages/demo/src/scripts/app.ts
================================================
import { Scroll } from '@scripts/classes/Scroll';
import { $screenDebounce } from "../stores/screen";
import { setViewportSize } from './utils/setViewportSize';
// Initialize the Scroll class
Scroll.init();
$screenDebounce.subscribe(() => {
setViewportSize();
});
// Progress event
const $progressEventLabel = document.querySelector('[data-custom-event="progress"]') as HTMLElement
const onProgressEventCall = (e: CustomEvent) => {
const { progress } = e.detail;
$progressEventLabel.textContent = `${Math.round(
(progress + Number.EPSILON) * 100
)}%`;
}
window.addEventListener('progressEvent', onProgressEventCall as EventListener);
// Progress position
const $positionProgresses = Array.from(document.querySelectorAll('[data-position-progress]'))
const onProgressPositionCall = (e: CustomEvent) => {
const { target, progress } = e.detail;
const $positionProgress = $positionProgresses.find(($el) => $el.parentElement === target) as HTMLElement;
$positionProgress.textContent = `${Math.round(
(progress + Number.EPSILON) * 100
)}%`;
}
window.addEventListener('progressPositionEvent', onProgressPositionCall as EventListener);
// Custom event
const $customEventLabel = document.querySelector('[data-custom-event="event"]') as HTMLElement
const onCustomEventCall = (e: CustomEvent) => {
const { way } = e.detail;
$customEventLabel.textContent = `scrollEvent ${way}`;
}
window.addEventListener('scrollEvent', onCustomEventCall as EventListener);
================================================
FILE: packages/demo/src/scripts/classes/Scroll.ts
================================================
import { isScrollStopped } from '@root/src/stores/scroll';
import LocomotiveScroll from '../../../../lib/index';
import type {
ILenisScrollToOptions,
lenisTargetScrollTo
} from '../../../../lib/types';
export class Scroll {
static locomotiveScroll: LocomotiveScroll;
static lastProgress: number;
static scrollOrientation: number;
// =============================================================================
// Lifecycle
// =============================================================================
static init() {
const scrollOrientation = document.documentElement.dataset.scrollOrientationSettings as 'vertical' | 'horizontal';
this.locomotiveScroll = new LocomotiveScroll({
lenisOptions: {
orientation: scrollOrientation,
},
scrollCallback: ({ progress }) => {
if (progress > this.lastProgress) {
if (this.scrollOrientation != 1) {
this.scrollOrientation = 1;
document.documentElement.style.setProperty(
'--scroll-direction',
this.scrollOrientation.toString()
);
}
} else if (this.scrollOrientation != -1) {
this.scrollOrientation = -1;
document.documentElement.style.setProperty(
'--scroll-direction',
this.scrollOrientation.toString()
);
}
this.lastProgress = progress as number;
}
});
isScrollStopped.listen((value) => {
if (value) {
this.stop();
} else {
this.start();
}
});
}
static destroy() {
this.locomotiveScroll?.destroy();
}
// =============================================================================
// Methods
// =============================================================================
static start() {
this.locomotiveScroll?.start();
}
static stop() {
this.locomotiveScroll?.stop();
}
static addScrollElements(container: HTMLElement) {
this.locomotiveScroll?.addScrollElements(container);
}
static removeScrollElements(container: HTMLElement) {
this.locomotiveScroll?.removeScrollElements(container);
}
static scrollTo(target: lenisTargetScrollTo, options?: ILenisScrollToOptions) {
this.locomotiveScroll?.scrollTo(target, options);
}
}
================================================
FILE: packages/demo/src/scripts/utils/maths.ts
================================================
const mapRange = (min: number, max: number, nmin: number, nmax: number, value: number) => {
return ((value - min) / (max - min)) * (nmax - nmin) + nmin;
};
const clamp = (min: number, max: number, value: number) => {
return Math.max(min, Math.min(value, max));
};
const normalize = (min: number, max: number, value: number) => {
return clamp(0, 1, (value - min) / (max - min));
};
const roundToDecimals = (value: number, decimals: number): number => {
const factor = Math.pow(10, decimals);
return Math.round((value + Number.EPSILON) * factor) / factor;
};
export { mapRange, clamp, normalize, roundToDecimals };
================================================
FILE: packages/demo/src/scripts/utils/setViewportSize.ts
================================================
const SUPPORTS_VH: boolean =
'CSS' in window &&
'supports' in window.CSS &&
window.CSS.supports('height: 100svh') &&
window.CSS.supports('height: 100dvh') &&
window.CSS.supports('height: 100lvh');
export const setViewportSize = () => {
// Document styles
const documentStyles = document.documentElement.style;
// Viewport width
const vw: number = document.body.clientWidth * 0.01;
documentStyles.setProperty('--vw', `${vw}px`);
// Return if browser supports vh, svh, dvh, & lvh
// if (SUPPORTS_VH) {
// return;
// }
// Viewport height
const svh: number = document.documentElement.clientHeight * 0.01;
documentStyles.setProperty('--svh', `${svh}px`);
const dvh: number = window.innerHeight * 0.01;
documentStyles.setProperty('--dvh', `${dvh}px`);
if (document.body) {
const fixed = document.createElement('div');
fixed.style.width = '1px';
fixed.style.height = '100vh';
fixed.style.position = 'fixed';
fixed.style.left = '0';
fixed.style.top = '0';
fixed.style.bottom = '0';
fixed.style.visibility = 'hidden';
document.body.appendChild(fixed);
const fixedHeight: number = fixed.clientHeight;
fixed.remove();
const lvh: number = fixedHeight * 0.01;
documentStyles.setProperty('--lvh', `${lvh}px`);
}
};
================================================
FILE: packages/demo/src/scripts/utils/string.ts
================================================
const toDash = (str: string) =>
str
.split(/(?=[A-Z])/)
.join('-')
.toLowerCase();
export { toDash };
================================================
FILE: packages/demo/src/stores/screen.ts
================================================
import { map } from 'nanostores';
import { debounce } from 'ts-debounce';
export interface IScreenValues {
width: number;
height: number;
}
export interface IScreenDebounceValues {
width: number;
height: number;
}
export const $screen = map({
width: window.innerWidth,
height: window.innerHeight
});
export const $screenDebounce = map({
width: window.innerWidth,
height: window.innerHeight
});
window.addEventListener('resize', () => {
$screen.setKey('width', window.innerWidth);
$screen.setKey('height', window.innerHeight);
});
const debouncedFunction: any = () => {
$screenDebounce.setKey('width', window.innerWidth);
$screenDebounce.setKey('height', window.innerHeight);
};
window.addEventListener('resize', debounce(debouncedFunction, 200));
================================================
FILE: packages/demo/src/stores/scroll.ts
================================================
import { atom } from "nanostores";
export const isScrollStopped = atom(false);
================================================
FILE: packages/demo/src/styles/main.scss
================================================
// ==========================================================================
// Tailwind CSS
// ==========================================================================
/**
* This injects Tailwind's base styles and any base styles registered by
* plugins.
*/
@tailwind base;
/**
* This injects Tailwind's component classes and any component classes
* registered by plugins.
*/
@tailwind components;
/**
* This injects Tailwind's utility classes and any utility classes registered
* by plugins.
*/
@tailwind utilities;
// ==========================================================================
// Fonts imports
// ==========================================================================
@layer base {
// @font-face {
// font-family: 'IBM Plex Mono';
// src: url('/fonts/IBMPlexMono-Regular.woff2') format('woff2');
// font-weight: normal;
// font-style: normal;
// font-display: swap;
// }
}
// ==========================================================================
// Vendors
// ==========================================================================
@import '../../../lib/bundled/locomotive-scroll.css';
// ==========================================================================
// Local files imports
// ==========================================================================
@import 'tools/maths';
@import 'tools/functions';
// ==========================================================================
// Global styles
// ==========================================================================
:root {
--color-primary: color('black');
--color-secondary: color('white');
}
html {
font-family: theme('fontFamily.sans');
}
body {
background: var(--color-secondary);
color: var(--color-primary);
}
::selection {
background-color: var(--color-primary);
color: var(--color-secondary);
text-shadow: none;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Define a transition duration during page visits */
html.is-changing .transition-fade {
transition: opacity 0.25s;
opacity: 1;
}
/* Define the styles for the unloaded pages */
html.is-animating .transition-fade {
opacity: 0;
}
html[data-scroll-orientation='horizontal'] {
body {
width: fit-content;
}
main {
display: flex;
}
}
:root {
--color-1: #f4f4ed;
--color-2: #6decaf;
--color-3: #357ded;
--color-4: #5e239d;
--color-5: #f61067;
}
.c-scroll {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
}
.c-scroll_offset {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
&:before,
&:after {
content: '';
position: absolute;
z-index: 1;
html[data-scroll-orientation='vertical'] & {
width: 100%;
height: 1px;
}
html[data-scroll-orientation='horizontal'] & {
width: 1px;
height: 100%;
}
}
&:before {
background-color: rgb(217, 66, 66);
html[data-scroll-orientation='vertical'] & {
bottom: var(--offset-start);
}
html[data-scroll-orientation='horizontal'] & {
right: var(--offset-start);
}
}
&:after {
background-color: rgb(74, 166, 215);
html[data-scroll-orientation='vertical'] & {
top: var(--offset-end);
}
html[data-scroll-orientation='horizontal'] & {
left: var(--offset-end);
}
}
}
.c-scroll_section {
width: 100%;
background-color: var(--color-1);
&.-full {
height: calc(100 * var(--svh));
html[data-scroll-orientation='horizontal'] & {
width: 100vw;
flex-shrink: 0;
}
}
&.-centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
&.-row {
flex-direction: row;
html[data-scroll-orientation='horizontal'] & {
flex-direction: column;
}
}
}
.c-scroll_box {
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 20vw;
height: 20vw;
max-width: 300px;
max-height: 300px;
overflow: hidden;
html[data-scroll-orientation='horizontal'] & {
width: 24vh;
height: 24vh;
}
img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.c-scroll-opacity {
opacity: 1 !important;
transition: opacity 0.75s ease();
}
.c-scroll-rotate {
transform: rotate(2turn) !important;
transition: transform 0.75s ease();
}
.c-scroll-direction {
html[data-scroll-orientation='vertical'] & {
transform: scaleY(var(--scroll-direction));
}
html[data-scroll-orientation='horizontal'] & {
transform: scaleX(var(--scroll-direction)) rotate(-90deg);
}
}
.c-scroll-toggler {
position: fixed;
top: 0;
left: 0;
display: inline-block;
background-color: grey;
padding: 0.5em;
}
================================================
FILE: packages/demo/src/styles/tools/functions.scss
================================================
// ==========================================================================
// Tools / Functions
// ==========================================================================
// Returns calculation of a percentage of the viewport small height.
//
// ```scss
// .c-box {
// height: svh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in svh
@function svh($number) {
@return calc(#{$number} * var(--svh, 1svh));
}
// Returns calculation of a percentage of the viewport large height.
//
// ```scss
// .c-box {
// height: lvh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in lvh
@function lvh($number) {
@return calc(#{$number} * var(--lvh, 1lvh));
}
// Returns calculation of a percentage of the viewport dynamic height.
//
// ```scss
// .c-box {
// height: dvh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in dvh
@function dvh($number) {
@return calc(#{$number} * var(--dvh, 1dvh));
}
// Returns calculation of a percentage of the viewport width.
//
// ```scss
// .c-box {
// width: vw(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in vw
@function vw($number) {
@return calc(#{$number} * var(--vw, 1vw));
}
================================================
FILE: packages/demo/src/styles/tools/maths.scss
================================================
// ==========================================================================
// Tools / Maths
// ==========================================================================
// Remove the unit of a length
//
// @param {Number} $number Number to remove unit from
// @return {function}
@function strip-unit($value) {
@if type-of($value) != 'number' {
@error "Invalid `#{type-of($value)}` type. Choose a number type instead.";
} @else if type-of($value) == 'number' and not is-unitless($value) {
@return math.div($value, $value * 0 + 1);
}
@return $value;
}
// Returns the square root of the given number.
//
// @param {number} $number The number to calculate.
// @return {number}
@function sqrt($number) {
$x: 1;
$value: $x;
@for $i from 1 through 10 {
$value: $x - math.div(($x * $x - abs($number)), (2 * $x));
$x: $value;
}
@return $value;
}
// Returns a number raised to the power of an exponent.
//
// @param {number} $number The base number.
// @param {number} $exp The exponent.
// @return {number}
@function pow($number, $exp) {
$value: 1;
@if $exp >0 {
@for $i from 1 through $exp {
$value: $value * $number;
}
} @else if $exp < 0 {
@for $i from 1 through -$exp {
$value: math.div($value, $number);
}
}
@return $value;
}
// Returns the factorial of the given number.
//
// @param {number} $number The number to calculate.
// @return {number}
@function fact($number) {
$value: 1;
@if $number >0 {
@for $i from 1 through $number {
$value: $value * $i;
}
}
@return $value;
}
// Returns an approximation of pi, with 11 decimals.
//
// @return {number}
@function pi() {
@return 3.14159265359;
}
// Converts the number in degrees to the radian equivalent .
//
// @param {number} $angle The angular value to calculate.
// @return {number} If $angle has the `deg` unit,
// the radian equivalent is returned.
// Otherwise, the unitless value of $angle is returned.
@function rad($angle) {
$unit: unit($angle);
$angle: strip-units($angle);
// If the angle has `deg` as unit, convert to radians.
@if ($unit ==deg) {
@return math.div($angle, 180) * pi();
}
@return $angle;
}
// Returns the sine of the given number.
//
// @param {number} $angle The angle to calculate.
// @return {number}
@function sin($angle) {
$sin: 0;
$angle: rad($angle);
@for $i from 0 through 10 {
$sin: $sin + pow(-1, $i) * math.div(pow($angle, (2 * $i + 1)), fact(2 * $i + 1));
}
@return $sin;
}
// Returns the cosine of the given number.
//
// @param {string} $angle The angle to calculate.
// @return {number}
@function cos($angle) {
$cos: 0;
$angle: rad($angle);
@for $i from 0 through 10 {
$cos: $cos + pow(-1, $i) * math.div(pow($angle, 2 * $i), fact(2 * $i));
}
@return $cos;
}
// Returns the tangent of the given number.
//
// @param {string} $angle The angle to calculate.
// @return {number}
@function tan($angle) {
@return math.div(sin($angle), cos($angle));
}
================================================
FILE: packages/demo/tailwind.config.ts
================================================
import defaultTheme from 'tailwindcss/defaultTheme';
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
prefix: 'u-',
corePlugins: {
container: false,
},
theme: {
extend: {
fontFamily: {
serif: [
'Times New Roman',
...defaultTheme.fontFamily.serif
],
sans: [
'Arial',
...defaultTheme.fontFamily.sans
],
},
colors: {
black: '#000000',
white: '#ffffff',
primary: '#312dfb',
},
screens: {
'to-2xs': { 'max': '339px' },
'2xs': '340px',
'to-xs': { 'max': '499px' },
'xs': '500px',
'to-sm': { 'max': '699px' },
'sm': '700px',
'to-md': { 'max': '999px' },
'md': '1000px',
'to-lg': { 'max': '1199px' },
'lg': '1200px',
'to-xl': { 'max': '1399px' },
'xl': '1400px',
'to-2xl': { 'max': '1599px' },
'2xl': '1600px',
'to-3xl': { 'max': '1799px' },
'3xl': '1800px',
'to-4xl': { 'max': '1999px' },
'4xl': '2000px',
'to-5xl': { 'max': '2399px' },
'5xl': '2400px',
},
gap: {
gutter: '20px',
gutterMobile: '10px'
},
transitionDuration: {
fast: '0.2s',
default: '0.4s',
slow: '0.6s',
slower: '0.8s',
slowest: '1s',
},
transitionTimingFunction: {
// Smooth
default: 'cubic-bezier(0.380, 0.005, 0.215, 1)',
// // Common easings
// power1: {
// in: 'cubic-bezier(0.550, 0.085, 0.680, 0.530)',
// out: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
// inOut: 'cubic-bezier(0.455, 0.030, 0.515, 0.955)',
// },
// power2: {
// in: 'cubic-bezier(0.550, 0.055, 0.675, 0.190)',
// out: 'cubic-bezier(0.215, 0.610, 0.355, 1.000)',
// inOut: 'cubic-bezier(0.645, 0.045, 0.355, 1.000)',
// },
// power3: {
// in: 'cubic-bezier(0.895, 0.030, 0.685, 0.220)',
// out: 'cubic-bezier(0.165, 0.840, 0.440, 1.000)',
// inOut: 'cubic-bezier(0.770, 0.000, 0.175, 1.000)',
// },
// power4: {
// in: 'cubic-bezier(0.755, 0.050, 0.855, 0.060)',
// out: 'cubic-bezier(0.230, 1.000, 0.320, 1.000)',
// inOut: 'cubic-bezier(0.860, 0.000, 0.070, 1.000)',
// },
// expo: {
// in: 'cubic-bezier(0.950, 0.050, 0.795, 0.035)',
// out: 'cubic-bezier(0.190, 1.000, 0.220, 1.000)',
// inOut: 'cubic-bezier(1.000, 0.000, 0.000, 1.000)',
// },
// back: {
// in: 'cubic-bezier(0.600, -0.280, 0.735, 0.045)',
// out: 'cubic-bezier(0.175, 00.885, 0.320, 1.275)',
// inOut: 'cubic-bezier(0.680, -0.550, 0.265, 1.550)',
// },
// sine: {
// in: 'cubic-bezier(0.470, 0.000, 0.745, 0.715)',
// out: 'cubic-bezier(0.390, 0.575, 0.565, 1.000)',
// inOut: 'cubic-bezier(0.445, 0.050, 0.550, 0.950)',
// },
// circ: {
// in: 'cubic-bezier(0.600, 0.040, 0.980, 0.335)',
// out: 'cubic-bezier(0.075, 0.820, 0.165, 1.000)',
// inOut: 'cubic-bezier(0.785, 0.135, 0.150, 0.860)',
// },
// slow: {
// out: 'cubic-bezier(.04,1.15,0.4,.99)',
// },
// bounce: 'cubic-bezier(0.17, 0.67, 0.3, 1.33)',
// smooth: 'cubic-bezier(0.380, 0.005, 0.215, 1)',
},
zIndex: {
modal: '200',
header: '100',
above: '1',
default: '0',
below: '-1',
},
},
},
plugins: [],
} satisfies Config;
================================================
FILE: packages/demo/tsconfig.json
================================================
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "Bundler",
"paths": {
"@root/*": [
"./*"
],
"@src/*": [
"./src/*"
],
"@components/*": [
"./src/components/*"
],
"@layouts/*": [
"./src/layouts/*"
],
"@scripts/*": [
"./src/scripts/*"
],
"@styles/*": [
"./src/styles/*"
],
"@types/*": [
"types/*"
],
"@data/*": [
"./src/data/*"
]
}
}
}
================================================
FILE: packages/demo/types/global.d.ts
================================================
type Seo = {
title?: string;
description?: string;
social?: {
facebook?: {
title?: string;
image?: {
url?: string;
};
description?: string;
};
twitter?: {
creator?: string;
title?: string;
image?: {
url?: string;
};
description?: string;
};
},
advanced?: {
robots?: string[];
canonical?: string;
};
};
================================================
FILE: packages/demo/types/swup.d.ts
================================================
type VisitType = {
fragmentVisit: any;
to: {
html: string;
};
};
================================================
FILE: packages/docs/.gitignore
================================================
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: packages/docs/README.md
================================================
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER= yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
================================================
FILE: packages/docs/babel.config.js
================================================
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
================================================
FILE: packages/docs/docs/documentation/attributes.md
================================================
---
sidebar_position: 3
---
# Attributes
## data-scroll
Enable viewport detection on an element.
## data-scroll-position
- **Type:** `string`
- **Default:** `start,end`

This attribute specifies the trigger position of the element within the scroll container when using Locomotive Scroll. It accepts two values: one for the position when the element enters the viewport, and a second for the position when the element leaves the viewport.
The position is calculated relative to the **Lenis scroll container** (which defaults to `window`, but can be customized via `lenisOptions.wrapper`).
Accepted values are: `'start'`, `'middle'`, `'end'`.
### Examples


Here's an example of using Locomotive Scroll's data-scroll-position attribute on an HTML element:
```html
```
## data-scroll-offset
- **Type:** `string`
- **Default:** `0,0`

Specifies the trigger offset of the element within the viewport when using the Locomotive Scroll library. It takes two values: one for the offset when the element enters the viewport, and a second for the offset when the element leaves the viewport.
The offset can be defined in two ways:
- If specified in percentages, it is relative to the viewport height.
- If specified in pixels, it is an absolute value.
For example:
- `'100,50%'` represents an offset of `100` pixels for the enter position and `50%` of the viewport height for the leave position.
- `'25%, 15%'` represents an offset of `25%` of the viewport height for the enter position and `15%` of the viewport height for the leave position.
### Example

Here's an example of using Locomotive Scroll's data-scroll-offset attribute on an HTML element:
```html
```
## data-scroll-class
- **Type:** `string`
- **Default:** `is-inview`
Specifies a custom class to be applied to the element when its offset intersects with the viewport. The default class used is `is-inview`.
You can provide your own class name as a string value to customize the styling or behavior of the element when it becomes visible within the viewport.
## data-scroll-repeat
Specifies whether the element's in-view detection should repeat if it is declared.
By default, the in-view detection of elements is not repeated. **Simply declaring this attribute will enable the repeat behavior for in-view detection of the element.**
## data-scroll-speed
- **Type:** `number`
Specifies the parallax speed for the element. The speed is relative to the scroll container size (not pixels), making it predictable across different viewport sizes.
### How it works
The parallax displacement is calculated using:
```
displacement = progress × containerSize × speed × -1
```
Where:
- `containerSize` = Height (or width for horizontal) of the Lenis scroll container
- `progress` = Element's progress through the viewport (`-1` to `1` for normal elements, `0` to `1` for in-fold elements)
- `speed` = Your specified value
### Examples
**With `data-scroll-speed="1"`:**
- Normal element: Moves from `+containerHeight` to `-containerHeight` (total = 2× container height)
- In-fold element: Moves from `0` to `-containerHeight` (total = 1× container height)
**With `data-scroll-speed="0.5"`:**
- Normal element: Total displacement = 1× container height
- In-fold element: Total displacement = 0.5× container height
**With `data-scroll-speed="-0.3"` (negative = reversed):**
- Element moves in opposite direction
- Total displacement = 0.6× container height
### Touch devices
Parallax is **automatically disabled on touch devices** by default for better native scrolling performance. To enable it on mobile/tablets, add the [`data-scroll-enable-touch-speed`](#data-scroll-enable-touch-speed) attribute.
:::tip
Start with small values like `0.1` to `0.5` for subtle effects. The value is now relative to container size, so effects scale naturally with viewport changes.
:::
## data-scroll-call
- **Type:** `string`
The `data-scroll-call` attribute enables you to trigger a custom event when an element becomes visible within the viewport. This attribute requires a string value that specifies the name of the custom event that you want to trigger.
By utilizing the `data-scroll-call` attribute, you can define and trigger your own events to perform specific actions or handle certain behaviors when elements scroll into view. These events can be listened to and handled in your JavaScript code using event listeners or any event handling mechanism provided by your framework or library.
Here's an example of how to use the `data-scroll-call` attribute:
```html
Trigger
```
```js
window.addEventListener('scrollEvent', (e) => {
const { target, way, from } = e.detail;
console.log(`target: ${target}`, `way: ${way}`, `from: ${from}`);
});
```
## data-scroll-css-progress
If you declare this attribute, it will add a CSS variable `--progress` to the element. This variable represents the current progress of the element and ranges between `0` and `1`.
By adding `--progress` as a CSS variable, you can utilize it in your CSS styles to create dynamic effects or animations based on the scrolling progress of the element.
## data-scroll-event-progress
- **Type:** `string`
When you declare this attribute, it will trigger the custom event that you specify. This event allows you to retrieve the current progress of the element, which ranges between `0` and `1`.
By utilizing the custom event, you can implement event handlers in your JavaScript code to perform actions or retrieve information based on the scrolling progress of the element.
```html
///
/// We can also manipulate entire layout systems by adding a series of modifiers
/// to the `.o-layout` block. For example:
///
/// @example
///
///
/// This will reverse the displayed order of the system so that it runs in the
/// opposite order to our source, effectively flipping the system over.
///
/// @example
///
///
/// This will cause the system to fill up from either the centre or the right
/// hand side. Default behaviour is to fill up the layout system from the left.
///
/// @requires tools/layout
/// @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss
////
.o-layout {
@include o-layout;
// Gutter modifiers
&.-gutter {
margin-left: rem(-$unit);
}
&.-gutter-small {
margin-left: rem(-$unit-small);
}
// Horizontal aligment modifiers
&.-center {
text-align: center;
}
&.-right {
text-align: right;
}
&.-reverse {
direction: rtl;
&.-flex {
flex-direction: row-reverse;
}
}
&.-flex {
display: flex;
&.-top {
align-items: flex-start;
}
&.-middle {
align-items: center;
}
&.-bottom {
align-items: flex-end;
}
}
&.-stretch {
align-items: stretch;
}
}
.o-layout_item {
@include o-layout_item;
// Gutter modifiers
.o-layout.-gutter > & {
padding-left: rem($unit);
}
.o-layout.-gutter-small > & {
padding-left: rem($unit-small);
}
// Vertical alignment modifiers
.o-layout.-middle > & {
vertical-align: middle;
}
.o-layout.-bottom > & {
vertical-align: bottom;
}
// Horizontal aligment modifiers
.o-layout.-center > &,
.o-layout.-right > &,
.o-layout.-reverse > & {
text-align: left;
}
.o-layout.-reverse > & {
direction: ltr;
}
}
================================================
FILE: packages/landing/assets/styles/objects/_ratio.scss
================================================
// ==========================================================================
// Objects / Ratio
// ==========================================================================
// Create ratio-bound content blocks, to keep media (e.g. images, videos) in
// their correct aspect ratios.
//
// http://alistapart.com/article/creating-intrinsic-ratios-for-video
//
// 1. Default cropping is a 1:1 ratio (i.e. a perfect square).
.o-ratio {
position: relative;
display: block;
overflow: hidden;
&:before {
display: block;
padding-bottom: 100%; // [1]
width: 100%;
content: "";
}
}
.o-ratio_content,
.o-ratio > img,
.o-ratio > iframe,
.o-ratio > embed,
.o-ratio > object {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
// height: 100%;
}
================================================
FILE: packages/landing/assets/styles/objects/_table.scss
================================================
// ==========================================================================
// Objects / Tables
// ==========================================================================
.o-table {
width: 100%;
// Force all cells within a table to occupy the same width as each other.
//
// @link https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout#Values
&.-fixed {
table-layout: fixed;
}
}
================================================
FILE: packages/landing/assets/styles/settings/_config.breakpoints.scss
================================================
// ==========================================================================
// Settings / Config / Breakpoints
// ==========================================================================
@use "sass:map";
// Breakpoints
// ==========================================================================
$breakpoints: (
"tiny": 500px,
"small": 700px,
"medium": 1000px,
"large": 1200px,
"big": 1400px,
"figma": 1440px,
"huge": 1600px,
"enormous": 1800px,
"gigantic": 2000px,
"colossal": 2400px
);
// Functions
// ==========================================================================
// Creates a min-width or max-width media query expression.
//
// @param {string} $breakpoint The breakpoint.
// @param {string} $type Either "min" or "max".
// @return {string}
@function mq($breakpoint, $type: "min") {
@if not map.has-key($breakpoints, $breakpoint) {
@warn "Unknown media query breakpoint: `#{$breakpoint}`";
}
$value: map.get($breakpoints, $breakpoint);
@if ($type == "min") {
@return "(min-width: #{$value})";
}
@if ($type == "max") {
@return "(max-width: #{$value - 1px})";
}
@error "Unknown media query type: #{$type}";
}
// Creates a min-width media query expression.
//
// @param {string} $breakpoint The breakpoint.
// @return {string}
@function mq-min($breakpoint) {
@return mq($breakpoint, "min");
}
// Creates a max-width media query expression.
//
// @param {string} $breakpoint The breakpoint.
// @return {string}
@function mq-max($breakpoint) {
@return mq($breakpoint, "max");
}
// Creates a min-width and max-width media query expression.
//
// @param {string} $from The min-width breakpoint.
// @param {string} $until The max-width breakpoint.
// @return {string}
@function mq-between($breakpointMin, $breakpointMax) {
@return "#{mq-min($breakpointMin)} and #{mq-max($breakpointMax)}";
}
// Legacy
// ==========================================================================
$from-tiny: map.get($breakpoints, "tiny") !default;
$to-tiny: map.get($breakpoints, "tiny") - 1 !default;
$from-small: map.get($breakpoints, "small") !default;
$to-small: map.get($breakpoints, "small") - 1 !default;
$from-medium: map.get($breakpoints, "medium") !default;
$to-medium: map.get($breakpoints, "medium") - 1 !default;
$from-large: map.get($breakpoints, "large") !default;
$to-large: map.get($breakpoints, "large") - 1 !default;
$from-big: map.get($breakpoints, "big") !default;
$to-big: map.get($breakpoints, "big") - 1 !default;
$from-figma: map.get($breakpoints, "figma") !default;
$to-figma: map.get($breakpoints, "figma") - 1 !default;
$from-huge: map.get($breakpoints, "huge") !default;
$to-huge: map.get($breakpoints, "huge") - 1 !default;
$from-enormous: map.get($breakpoints, "enormous") !default;
$to-enormous: map.get($breakpoints, "enormous") - 1 !default;
$from-gigantic: map.get($breakpoints, "gigantic") !default;
$to-gigantic: map.get($breakpoints, "gigantic") - 1 !default;
$from-colossal: map.get($breakpoints, "colossal") !default;
$to-colossal: map.get($breakpoints, "colossal") - 1 !default;
================================================
FILE: packages/landing/assets/styles/settings/_config.colors.scss
================================================
// ==========================================================================
// Settings / Config / Colors
// ==========================================================================
@use "sass:map";
@use "sass:color";
// Palette
// ==========================================================================
$colors: (
'blue': #202ded,
'white': #FFFFFF,
'black': #000000,
'red': #F4574D,
);
// Function
// ==========================================================================
// Returns color code.
//
// ```scss
// .c-box {
// color: color(blue);
// }
// ```
//
// @param {string} $key - The color key in $colors.
// @param {number} $alpha - The alpha for the color value.
// @return {color}
@function color($key, $alpha: 1) {
$key: #{$key}; // Force string conversion
@if not map.has-key($colors, $key) {
@error "Unknown '#{$key}' in $colors.";
}
@if($alpha < 0 or $alpha > 1) {
@error "Alpha '#{$alpha}' must be in range [0, 1].";
}
$color: map.get($colors, $key);
@return rgba($color, $alpha);
}
// Specifics
// ==========================================================================
// Link
$color-link: color(blue);
$color-link-focus: color(blue);
$color-link-hover: color.adjust(color(blue), $lightness: -10%);
// Selection
$color-selection-text: color(black);
$color-selection-background: color(white);
// Socials
$color-facebook: #3B5998;
$color-instagram: #E1306C;
$color-youtube: #CD201F;
$color-twitter: #1DA1F2;
================================================
FILE: packages/landing/assets/styles/settings/_config.eases.scss
================================================
// ==========================================================================
// Settings / Config / Eases
// ==========================================================================
@use "sass:map";
// Eases
// ==========================================================================
$eases: (
// Power 1
"power1.in": cubic-bezier(0.550, 0.085, 0.680, 0.530),
"power1.out": cubic-bezier(0.250, 0.460, 0.450, 0.940),
"power1.inOut": cubic-bezier(0.455, 0.030, 0.515, 0.955),
// Power 2
"power2.in": cubic-bezier(0.550, 0.055, 0.675, 0.190),
"power2.out": cubic-bezier(0.215, 0.610, 0.355, 1.000),
"power2.inOut": cubic-bezier(0.645, 0.045, 0.355, 1.000),
// Power 3
"power3.in": cubic-bezier(0.895, 0.030, 0.685, 0.220),
"power3.out": cubic-bezier(0.165, 0.840, 0.440, 1.000),
"power3.inOut": cubic-bezier(0.770, 0.000, 0.175, 1.000),
// Power 4
"power4.in": cubic-bezier(0.755, 0.050, 0.855, 0.060),
"power4.out": cubic-bezier(0.230, 1.000, 0.320, 1.000),
"power4.inOut": cubic-bezier(0.860, 0.000, 0.070, 1.000),
// Expo
"expo.in": cubic-bezier(0.950, 0.050, 0.795, 0.035),
"expo.out": cubic-bezier(0.190, 1.000, 0.220, 1.000),
"expo.inOut": cubic-bezier(1.000, 0.000, 0.000, 1.000),
// Back
"back.in": cubic-bezier(0.600, -0.280, 0.735, 0.045),
"back.out": cubic-bezier(0.175, 00.885, 0.320, 1.275),
"back.inOut": cubic-bezier(0.680, -0.550, 0.265, 1.550),
// Sine
"sine.in": cubic-bezier(0.470, 0.000, 0.745, 0.715),
"sine.out": cubic-bezier(0.390, 0.575, 0.565, 1.000),
"sine.inOut": cubic-bezier(0.445, 0.050, 0.550, 0.950),
// Circ
"circ.in": cubic-bezier(0.600, 0.040, 0.980, 0.335),
"circ.out": cubic-bezier(0.075, 0.820, 0.165, 1.000),
"circ.inOut": cubic-bezier(0.785, 0.135, 0.150, 0.860),
// Misc
"bounce": cubic-bezier(0.17, 0.67, 0.3, 1.33),
"slow.out": cubic-bezier(.04,1.15,0.4,.99),
"smooth": cubic-bezier(0.380, 0.005, 0.215, 1),
);
// Default value for ease()
$ease-default: "power2.out" !default;
// Function
// ==========================================================================
// Returns ease curve.
//
// ```scss
// .c-box {
// transition-timing-function: ease("power2.out");
// }
// ```
//
// @param {string} $key - The ease key in $eases.
// @return {easing-function}
@function ease($key: $ease-default) {
@if not map.has-key($eases, $key) {
@error "Unknown '#{$key}' in $eases.";
}
@return map.get($eases, $key);
}
================================================
FILE: packages/landing/assets/styles/settings/_config.fonts.scss
================================================
// ==========================================================================
// Settings / Config / Breakpoints
// ==========================================================================
@use "sass:list";
@use "sass:map";
@use "sass:meta";
// Font fallbacks (retrieved from systemfontstack.com on 2022-05-31)
// ==========================================================================
$font-fallback-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
$font-fallback-serif: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
$font-fallback-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
// Typefaces
// ==========================================================================
// List of custom font faces as tuples.
//
// ```
//
// ```
$font-faces: (
("Helvetica Now Display", "HelveticaNowDisplay-Medium", 500, normal),
("Helvetica Now Display", "HelveticaNowDisplay-Regular", 400, normal),
("PP Locomotive New", "PPLocomotiveNew-Light", 300, normal)
);
// Map of font families.
//
// ```
// : (, )
// ```
$font-families: (
display: list.join("Helvetica Now Display", $font-fallback-sans, $separator: comma),
serif: list.join("PP Locomotive New", $font-fallback-sans, $separator: comma),
);
// Font directory
$font-dir: "../fonts/";
// Functions
// ==========================================================================
// Imports the custom font.
//
// The mixin expects font files to be woff and woff2.
//
// @param {List} $webfont - A custom font to import, as a tuple:
// ``.
// @param {String} $dir - The webfont directory path.
// @output The `@font-face` at-rule specifying the custom font.
@mixin font-face($webfont, $dir) {
@font-face {
font-display: swap;
font-family: list.nth($webfont, 1);
src: url("#{$dir}#{list.nth($webfont, 2)}.woff2") format("woff2"),
url("#{$dir}#{list.nth($webfont, 2)}.woff") format("woff");
font-weight: #{list.nth($webfont, 3)};
font-style: #{list.nth($webfont, 4)};
}
}
// Imports the list of custom fonts.
//
// @require {mixin} font-face
//
// @param {List} $webfonts - List of custom fonts to import.
// See `font-face` mixin for details.
// @param {String} $dir - The webfont directory path.
// @output The `@font-face` at-rules specifying the custom fonts.
@mixin font-faces($webfonts, $dir) {
@if (list.length($webfonts) > 0) {
@if (meta.type-of(list.nth($webfonts, 1)) == "list") {
@each $webfont in $webfonts {
@include font-face($webfont, $dir);
}
} @else {
@include font-face($webfonts, $dir);
}
}
}
// Retrieves the font family stack for the given font ID.
//
// @require {variable} $font-families - See settings directory.
//
// @param {String} $font-family - The custom font ID.
// @throws Error if the $font-family does not exist.
// @return {List} The font stack.
@function ff($font-family) {
@if not map.has-key($font-families, $font-family) {
@error "No font-family found in $font-families map for `#{$font-family}`.";
}
$value: map.get($font-families, $font-family);
@return $value;
}
================================================
FILE: packages/landing/assets/styles/settings/_config.scss
================================================
// ==========================================================================
// Settings / Config
// ==========================================================================
@use "sass:math";
@use "config.colors" as *;
@use "config.timings" as *;
@use "config.eases" as *;
// Context
// =============================================================================
// The current stylesheet context. Available values: frontend, editor.
$context: frontend !default;
// Path is relative to the stylesheets directory.
$assets-path: "../" !default;
// Typography
// =============================================================================
// Base
$font-size: 16px;
$line-height: math.div(24px, $font-size);
$font-color: color(black);
// Weights
$font-weight-light: 300;
$font-weight-regular: 400;
$font-weight-medium: 500;
// Transition defaults
// =============================================================================
$speed: t(normal);
$easing: ease("power3.out");
// Spacing Units
// =============================================================================
$unit: 60px;
$unit-small: 20px;
// Container
// ==========================================================================
$padding: $unit;
// Grid
// ==========================================================================
$base-column-nb: 4;
================================================
FILE: packages/landing/assets/styles/settings/_config.spacers.scss
================================================
// ==========================================================================
// Settings / Config / Spacers
// ==========================================================================
@use "sass:map";
@use "../tools/functions" as *;
:root {
--spacing-2xs-mobile: 6;
--spacing-2xs-desktop: 10;
--spacing-xs-mobile: 14;
--spacing-xs-desktop: 16;
--spacing-sm-mobile: 28;
--spacing-sm-desktop: 32;
--spacing-md-mobile: 42;
--spacing-md-desktop: 56;
--spacing-lg-mobile: 72;
--spacing-lg-desktop: 96;
--spacing-xl-mobile: 90;
--spacing-xl-desktop: 120;
--spacing-2xl-mobile: 96;
--spacing-2xl-desktop: 160;
--spacing-3xl-mobile: 112;
--spacing-3xl-desktop: 224;
}
// Spacers
// ==========================================================================
$spacers: (
'gutter': var(--grid-gutter),
'2xs': #{spacingClamp('2xs')},
'xs': #{spacingClamp('xs')},
'sm': #{spacingClamp('sm')},
'md': #{spacingClamp('md')},
'lg': #{spacingClamp('lg')},
'xl': #{spacingClamp('xl')},
'2xl': #{spacingClamp('2xl')},
'3xl': #{spacingClamp('3xl')},
);
// Function
// ==========================================================================
// Returns spacer.
//
// ```scss
// .c-box {
// margin-top: spacer(gutter);
// }
// ```
//
// @param {string} $key - The spacer key in $spacers.
// @param {number} $multiplier - The multiplier of the spacer value.
// @return {size}
@function spacer($spacer: $spacer-default, $multiplier: 1) {
@if not map.has-key($spacers, $spacer) {
@error "Unknown master spacer: #{$spacer}";
}
$index: map.get($spacers, $spacer);
@return calc(#{$index} * #{$multiplier});
}
================================================
FILE: packages/landing/assets/styles/settings/_config.timings.scss
================================================
// ==========================================================================
// Settings / Config / Timings
// ==========================================================================
@use "sass:map";
// Timings
// ==========================================================================
$timings: (
fastest: 0.1s,
faster: 0.15s,
fast: 0.25s,
normal: 0.5s,
medium: 0.6s,
slow: 0.75s,
slower: 1s,
slowest: 2s,
);
// Default timing for t()
$timing-default: "normal" !default;
// Function
// ==========================================================================
// Returns timing.
//
// ```scss
// .c-box {
// transition-duration: t(slow);
// }
// ```
//
// @param {string} $key - The timing key in $timings.
// @return {duration}
@function t($key: $timing-default) {
@if not map.has-key($timings, $key) {
@error "Unknown '#{$key}' in $timings.";
}
@return map.get($timings, $key);
}
================================================
FILE: packages/landing/assets/styles/settings/_config.variables.scss
================================================
// ==========================================================================
// Settings / Config / CSS VARS
// ==========================================================================
@use '../tools/functions' as *;
@use 'config.breakpoints' as *;
:root {
// Grid
--grid-columns: 4;
--grid-gutter: #{rem(10px)};
--grid-margin: #{rem(10px)};
// Container
--container-width: calc(100% - 2 * var(--grid-margin));
@media (max-width: $to-small) {
--header-height: #{rem(34px)};
}
@media (min-width: $from-small) {
--grid-gutter: #{rem(16px)};
--grid-margin: #{rem(20px)};
--header-height: #{rem(60px)};
}
}
================================================
FILE: packages/landing/assets/styles/settings/_config.zindexes.scss
================================================
// ==========================================================================
// Settings / Config / Z-indexes
// ==========================================================================
@use "sass:map";
// Timings
// ==========================================================================
$z-indexes: (
"header": 200,
"above": 1,
"default": 0,
"below": -1
);
// Default z-index for z()
$z-index-default: "above" !default;
// Function
// ==========================================================================
// Retrieves the z-index from the {@see $layers master list}.
//
// @link on http://css-tricks.com/handling-z-index/
//
// @param {string} $layer The name of the z-index.
// @param {number} $modifier A positive or negative modifier to apply
// to the returned z-index value.
// @throw Error if the $layer does not exist.
// @throw Warning if the $modifier might overlap another master z-index.
// @return {number} The computed z-index of $layer and $modifier.
@function z($layer: $z-index-default, $modifier: 0) {
@if not map.has-key($z-indexes, $layer) {
@error "Unknown master z-index layer: #{$layer}";
}
@if ($modifier >= 50 or $modifier <= -50) {
@warn "Modifier may overlap the another master z-index layer: #{$modifier}";
}
$index: map.get($z-indexes, $layer);
@return $index + $modifier;
}
================================================
FILE: packages/landing/assets/styles/tools/_family.scss
================================================
// ==========================================================================
// Tools / Family
// ==========================================================================
@use "sass:math";
// DOCS : https://lukyvj.github.io/family.scss/
//
// Select all children from the first to `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin first($num) {
@if $num == 1 {
&:first-child {
@content;
}
} @else {
&:nth-child(-n + #{$num}) {
@content;
}
}
}
// Select all children from the last to `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin last($num) {
&:nth-last-child(-n + #{$num}) {
@content;
}
}
// Select all children after the first to `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin after-first($num) {
&:nth-child(n + #{$num + 1}) {
@content;
}
}
// Select all children before `$num` from the last.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin from-end($num) {
&:nth-last-child(#{$num}) {
@content;
}
}
// Select all children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin between($first, $last) {
&:nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all even children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin even-between($first, $last) {
&:nth-child(even):nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all odd children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin odd-between($first, $last) {
&:nth-child(odd):nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all `$num` children between `$first` and `$last`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin n-between($num, $first, $last) {
&:nth-child(#{$num}n):nth-child(n + #{$first}):nth-child(-n + #{$last}) {
@content;
}
}
// Select all children but `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin all-but($num) {
&:not(:nth-child(#{$num})) {
@content;
}
}
// Select children each `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
// @alias every
@mixin each($num) {
&:nth-child(#{$num}n) {
@content;
}
}
// Select children each `$num`.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin every($num) {
&:nth-child(#{$num}n) {
@content;
}
}
// Select the `$num` child from the start and the `$num` child from the last.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin from-first-last($num) {
&:nth-child(#{$num}),
&:nth-last-child(#{$num}) {
@content;
}
}
// Select the item in the middle of `$num` child. Only works with odd number
// chain.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin middle($num) {
&:nth-child(#{round(math.div($num, 2))}) {
@content;
}
}
// Select all children between the `$num` first and the `$num` last.
// @group with-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - id of the child
@mixin all-but-first-last($num) {
&:nth-child(n + #{$num}):nth-last-child(n + #{$num}) {
@content;
}
}
// This quantity-query mixin will only select the first of `$limit` items. It will not
// work if there is not as much as item as you set in `$limit`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin first-of($limit) {
&:nth-last-child(#{$limit}):first-child {
@content;
}
}
// This quantity-query mixin will only select the last of `$limit` items. It will not
// if there is not as much as item as you set in `$limit`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin last-of($limit) {
&:nth-of-type(#{$limit}):nth-last-of-type(1) {
@content;
}
}
// This quantity-query mixin will select every items if there is at least `$num` items. It will not
// if there is not as much as item as you set in `$num`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin at-least($num) {
$selector: &;
$child: nth(nth($selector, -1), -1);
&:nth-last-child(n + #{$num}),
&:nth-last-child(n + #{$num}) ~ #{$child} {
@content;
}
}
// This quantity-query mixin will select every items if there is at most `$num` items. It will not
// if there is not as much as item as you set in `$num`.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin at-most($num) {
$selector: &;
$child: nth(nth($selector, -1), -1);
&:nth-last-child(-n + #{$num}):first-child,
&:nth-last-child(-n + #{$num}):first-child ~ #{$child} {
@content;
}
}
// This quantity-query mixin will select every items only if there is between `$min` and `$max` items.
// @group Quantity queries
// @param {number} $limit
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin in-between($min, $max) {
$selector: &;
$child: nth(nth($selector, -1), -1);
&:nth-last-child(n + #{$min}):nth-last-child(-n + #{$max}):first-child,
&:nth-last-child(n + #{$min}):nth-last-child(-n + #{$max}):first-child ~ #{$child} {
@content;
}
}
// Select the first exact child
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin first-child() {
&:first-of-type {
@content
}
}
// Select the last exact child
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin last-child() {
&:last-of-type {
@content
}
}
// Select all even children.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin even() {
&:nth-child(even) {
@content;
}
}
// Select all odd children.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin odd() {
&:nth-child(odd) {
@content;
}
}
// Select only the first and last child.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin first-last() {
&:first-child,
&:last-child {
@content;
}
}
// Will only select the child if it’s unique.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @alias only
@mixin unique() {
&:only-child {
@content;
}
}
// Will only select the child if it’s unique.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin only() {
&:only-child {
@content;
}
}
// Will only select children if they are not unique. Meaning if there is at
// least 2 children, the style is applied.
// @group no-arguments
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
@mixin not-unique() {
&:not(:only-child) {
@content;
}
}
// This mixin is used to automatically sort z-index in numerical order. But it
// can also sort them in anti-numerical order, depending the parameters you use.
// @group using functions
// @content [Write the style you want to apply to the children, and it will be added within the @content directive]
// @param {number} $num - Number of children
// @param {string} $direction [forward] - Direction of the sort
// @param {number} $index [0] - Index of the sorting
@mixin child-index($num, $direction: 'forward', $index: 0) {
@for $i from 1 through $num {
@if ($direction == 'forward') {
&:nth-child(#{$i}) {
z-index: order-index($i, $index);
@content;
}
} @else if ($direction == 'backward') {
&:nth-last-child(#{$i}) {
z-index: order-index($i, $index);
@content;
}
}
}
}
// Used by the child-index mixin. It will returned the proper sorted numbers
// depending on the `$index` value.
// @access private
// @param {number} $num - Number of children
// @param {number} $index - Index of the sorting
@function order-index($i, $index) {
@return ($index + $i);
}
================================================
FILE: packages/landing/assets/styles/tools/_functions.scss
================================================
// ==========================================================================
// Tools / Functions
// ==========================================================================
@use "sass:math";
@use "sass:meta";
@use "sass:list";
@use "maths" as *;
// Default font size for em() and rem() functions
$font-size: 16px !default;
// Check if the given value is a number in pixel
//
// @param {Number} $number - The value to check
// @return {Boolean}
@function is-pixel-number($number) {
@return meta.type-of($number) == number and math.unit($number) == "px";
}
// Converts the given pixel value to its EM quivalent.
//
// @param {Number} $size - The pixel value to convert.
// @param {Number} $base [$font-size] - The assumed base font size.
// @return {Number} Scalable pixel value in EMs.
@function em($size, $base: $font-size) {
@if not is-pixel-number($size) {
@error "`#{$size}` needs to be a number in pixel.";
}
@if not is-pixel-number($base) {
@error "`#{$base}` needs to be a number in pixel.";
}
@return math.div($size, $base) * 1em;
}
// Converts the given pixel value to its REM quivalent.
//
// @param {Number} $size - The pixel value to convert.
// @param {Number} $base [$font-size] - The assumed base font size.
// @return {Number} Scalable pixel value in REMs.
@function rem($size, $base: $font-size) {
@if not is-pixel-number($size) {
@error "`#{$size}` needs to be a number in pixel.";
}
@if not is-pixel-number($base) {
@error "`#{$base}` needs to be a number in pixel.";
}
@return math.div($size, $base) * 1rem;
}
// Converts a number to a percentage.
//
// @alias percentage()
// @link http://sassdoc.com/annotations/#alias
// @param {Number} $number - The value to convert.
// @return {Number} A percentage.
@function span($number) {
@return percentage($number);
}
// Checks if a list contains a value(s).
//
// @link https://github.com/thoughtbot/bourbon/blob/master/core/bourbon/validators/_contains.scss
// @param {List} $list - The list to check against.
// @param {List} $values - A single value or list of values to check for.
// @return {Boolean}
// @access private
@function list-contains(
$list,
$values...
) {
@each $value in $values {
@if meta.type-of(list.index($list, $value)) != "number" {
@return false;
}
}
@return true;
}
// Resolve whether a rule is important or not.
//
// @param {Boolean} $flag - Whether a rule is important (TRUE) or not (FALSE).
// @return {String|Null} Returns `!important` or NULL.
@function important($flag: false) {
@if ($flag == true) {
@return !important;
} @else if ($flag == false) {
@return null;
} @else {
@error "`#{$flag}` needs to be `true` or `false`.";
}
}
// Determine if the current context is for a WYSIWYG editor.
//
// @requires {String} $context - The global context of the stylesheet.
// @return {Boolean} If the $context is set to "editor".
@function is-editor() {
@return ('editor' == $context);
}
// Determine if the current context is for the front-end.
//
// @requires {String} $context - The global context of the stylesheet.
// @return {Boolean} If the $context is set to "frontend".
@function is-frontend() {
@return ('frontend' == $context);
}
$context: 'frontend' !default;
// Returns calculation of a percentage of the grid cell width
// with optional inset of grid gutter.
//
// ```scss
// .c-box {
// width: grid-space(6/12);
// margin-left: grid-space(1/12, 1);
// }
// ```
//
// @param {number} $number - The percentage spacer
// @param {number} $inset - The grid gutter inset
// @return {function}
@function grid-space($percentage, $inset: 0) {
@return calc(#{$percentage} * (100vw - 2 * var(--grid-margin, 0px)) - (1 - #{$percentage}) * var(--grid-gutter, 0px) + #{$inset} * var(--grid-gutter, 0px));
}
// Returns calculation of a percentage of the viewport height.
//
// ```scss
// .c-box {
// height: vh(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in vh
@function vh($number) {
@return calc(#{$number} * var(--vh, 1vh));
}
// Returns calculation of a percentage of the viewport width.
//
// ```scss
// .c-box {
// width: vw(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in vw
@function vw($number) {
@return calc(#{$number} * var(--vw, 1vw));
}
// Returns calculation of a percentage of the viewport width.
//
// ```scss
// .c-box {
// width: vw(100);
// }
// ```
//
// @param {number} $number - The percentage number
// @return {function} in vw
$vw-viewport: 1440;
@function vw($number) {
@return calc(#{$number} * var(--vw, 1vw));
}
@function clampWithMax($min, $size, $max) {
$vw-context: $vw-viewport * 0.01;
@return clamp(#{$min}, calc(#{$size} / #{$vw-context} * 1vw), #{$max});
}
@function spacingClamp($size) {
@return clampWithMax(
calc(#{rem(1px)} * var(--spacing-#{$size}-mobile)),
var(--spacing-#{$size}-desktop),
calc(#{rem(1px)} * var(--spacing-#{$size}-desktop))
);
}
// Returns clamp of calculated preferred responsive font size
// within a font size and breakpoint range.
//
// ```scss
// .c-heading.-h1 {
// font-size: responsive-value(30px, 60px, 1800);
// }
//
// .c-heading.-h2 {
// font-size: responsive-value(20px, 40px, $from-big);
// }
// ```
//
// @param {number} $min-size - Minimum font size in pixels.
// @param {number} $max-size - Maximum font size in pixels.
// @param {number} $breakpoint - Maximum breakpoint.
// @return {function, number>}
@function responsive-value($min-size, $max-size, $breakpoint) {
$delta: math.div($max-size, $breakpoint);
@return clamp($min-size, calc(#{strip-unit($delta)} * #{vw(100)}), $max-size);
}
================================================
FILE: packages/landing/assets/styles/tools/_layout.scss
================================================
// ==========================================================================
// Tools / Layout
// ==========================================================================
@use "sass:meta";
// Grid-like layout system.
//
// The layout tools provide a column-style layout system. This file contains
// the mixins to generate basic structural elements.
//
// @link https://github.com/inuitcss/inuitcss/blob/0420ba8/objects/_objects.layout.scss
//
//
// Generate the layout container.
//
// 1. Use the negative margin trick for multi-row grids:
// http://csswizardry.com/2011/08/building-better-grid-systems/
//
// @requires {function} u-list-reset
// @output `font-size`, `margin`, `padding`, `list-style`
@mixin o-layout($gutter: 0, $fix-whitespace: true) {
margin: 0;
padding: 0;
list-style: none;
@if ($fix-whitespace) {
font-size: 0;
}
@if (meta.type-of($gutter) == number) {
margin-left: -$gutter; // [1]
}
}
// Generate the layout item.
//
// 1. Required in order to combine fluid widths with fixed gutters.
// 2. Allows us to manipulate grids vertically, with text-level properties,
// etc.
// 3. Default item alignment is with the tops of each other, like most
// traditional grid/layout systems.
// 4. By default, all layout items are full-width (mobile first).
// 5. Gutters provided by left padding:
// http://csswizardry.com/2011/08/building-better-grid-systems/
@mixin o-layout_item($gutter: 0, $fix-whitespace: true) {
display: inline-block; // [2]
width: 100%; // [4]
vertical-align: top; // [3]
@if ($fix-whitespace) {
font-size: 1rem;
}
@if (meta.type-of($gutter) == number) {
padding-left: $gutter; // [5]
}
}
================================================
FILE: packages/landing/assets/styles/tools/_maths.scss
================================================
// ==========================================================================
// Tools / Maths
// ==========================================================================
@use "sass:meta";
@use "sass:math";
// Remove the unit of a length
//
// @param {Number} $number Number to remove unit from
// @return {function}
@function strip-unit($value) {
@if meta.type-of($value) != "number" {
@error "Invalid `#{meta.type-of($value)}` type. Choose a number type instead.";
} @else if meta.type-of($value) == "number" and not math.is-unitless($value) {
@return math.div($value, $value * 0 + 1);
}
@return $value;
}
// Returns the square root of the given number.
//
// @param {number} $number The number to calculate.
// @return {number}
@function sqrt($number) {
$x: 1;
$value: $x;
@for $i from 1 through 10 {
$value: $x - math.div(($x * $x - abs($number)), (2 * $x));
$x: $value;
}
@return $value;
}
// Returns a number raised to the power of an exponent.
//
// @param {number} $number The base number.
// @param {number} $exp The exponent.
// @return {number}
@function pow($number, $exp) {
$value: 1;
@if $exp > 0 {
@for $i from 1 through $exp {
$value: $value * $number;
}
} @else if $exp < 0 {
@for $i from 1 through -$exp {
$value: math.div($value, $number);
}
}
@return $value;
}
// Returns the factorial of the given number.
//
// @param {number} $number The number to calculate.
// @return {number}
@function fact($number) {
$value: 1;
@if $number > 0 {
@for $i from 1 through $number {
$value: $value * $i;
}
}
@return $value;
}
// Returns an approximation of pi, with 11 decimals.
//
// @return {number}
@function pi() {
@return 3.14159265359;
}
// Converts the number in degrees to the radian equivalent .
//
// @param {number} $angle The angular value to calculate.
// @return {number} If $angle has the `deg` unit,
// the radian equivalent is returned.
// Otherwise, the unitless value of $angle is returned.
@function rad($angle) {
$unit: math.unit($angle);
$angle: strip-units($angle);
// If the angle has `deg` as unit, convert to radians.
@if ($unit == deg) {
@return math.div($angle, 180) * pi();
}
@return $angle;
}
// Returns the sine of the given number.
//
// @param {number} $angle The angle to calculate.
// @return {number}
@function sin($angle) {
$sin: 0;
$angle: rad($angle);
@for $i from 0 through 10 {
$sin: $sin + pow(-1, $i) * math.div(pow($angle, (2 * $i + 1)), fact(2 * $i + 1));
}
@return $sin;
}
// Returns the cosine of the given number.
//
// @param {string} $angle The angle to calculate.
// @return {number}
@function cos($angle) {
$cos: 0;
$angle: rad($angle);
@for $i from 0 through 10 {
$cos: $cos + pow(-1, $i) * math.div(pow($angle, 2 * $i), fact(2 * $i));
}
@return $cos;
}
// Returns the tangent of the given number.
//
// @param {string} $angle The angle to calculate.
// @return {number}
@function tan($angle) {
@return math.div(sin($angle), cos($angle));
}
@function mapRangePx($css-var, $min0, $max0, $min1, $max1) {
@return calc((#{$min1} + (( #{$css-var} - #{$min0}) / (#{$max0} - #{$min0})) * (#{$max1} - #{$min1})) * 1px);
}
@function mapRange($css-var, $min0, $max0, $min1, $max1) {
@return calc(#{$min1} + (( #{$css-var} - #{$min0}) / (#{$max0} - #{$min0})) * (#{$max1} - #{$min1}));
}
@function mapRangeClampPx($css-var, $min0, $max0, $min1, $max1, $clampMin, $clampMax) {
@return calc(clamp( (#{$clampMin}) * 1px, (#{$min1} + (( #{$css-var} - #{$min0}) / (#{$max0} - #{$min0})) * (#{$max1} - #{$min1})) * 1px, (#{$clampMax}) * 1px));
}
@function mapRangeClamp($css-var, $min0, $max0, $min1, $max1, $clampMin, $clampMax) {
@return calc(clamp( #{$clampMin}, #{$min1} + (( #{$css-var} - #{$min0}) / (#{$max0} - #{$min0})) * (#{$max1} - #{$min1}), #{$clampMax}));
}
================================================
FILE: packages/landing/assets/styles/tools/_mixins.scss
================================================
// ==========================================================================
// Tools / Mixins
// ==========================================================================
@use "sass:meta";
@use "sass:math";
@use "functions" as *;
// Set the color of the highlight that appears over a link while it's being tapped.
//
// By default, the highlight is suppressed.
//
// @param {Color} $value [rgba(0, 0, 0, 0)] - The value of the highlight.
// @output `-webkit-tap-highlight-color`
@mixin tap-highlight-color($value: rgba(0, 0, 0, 0)) {
-webkit-tap-highlight-color: $value;
}
// Set whether or not touch devices use momentum-based scrolling for the given element.
//
// By default, applies momentum-based scrolling for the current element.
//
// @param {String} $value [rgba(0, 0, 0, 0)] - The type of scrolling.
// @output `-webkit-overflow-scrolling`
@mixin overflow-scrolling($value: touch) {
-webkit-overflow-scrolling: $value;
}
// Micro clearfix rules for containing floats.
//
// @link http://www.cssmojo.com/the-very-latest-clearfix-reloaded/
// @param {List} $supports The type of clearfix to generate.
// @output Injects `:::after` pseudo-element.
@mixin u-clearfix($supports...) {
&::after {
display: meta.if(list-contains($supports, table), table, block);
clear: both;
content: meta.if(list-contains($supports, opera), " ", "");
}
}
// Generate a font-size and baseline-compatible line-height.
//
// @link https://github.com/inuitcss/inuitcss/c14029c/tools/_tools.font-size.scss
// @param {Number} $font-size - The size of the font.
// @param {Number} $line-height [auto] - The line box height.
// @param {Boolean} $important [false] - Whether the font-size is important.
// @output `font-size`, `line-height`
@mixin font-size($font-size, $line-height: auto, $important: false) {
$important: important($important);
font-size: rem($font-size) $important;
@if ($line-height == "auto") {
line-height: ceil(math.div($font-size, $line-height)) * math.div($line-height, $font-size) $important;
}
@else {
@if (meta.type-of($line-height) == number or $line-height == "inherit" or $line-height == "normal") {
line-height: $line-height $important;
}
@else if ($line-height != "none" and $line-height != false) {
@error "D’oh! `#{$line-height}` is not a valid value for `$line-height`.";
}
}
}
// Vertically-center the direct descendants of the current element.
//
// Centering is achieved by displaying children as inline-blocks. Any whitespace
// between elements is nullified by redefining the font size of the container
// and its children.
//
// @output `font-size`, `display`, `vertical-align`
@mixin o-vertical-center {
font-size: 0;
&::before {
display: inline-block;
height: 100%;
content: "";
vertical-align: middle;
}
> * {
display: inline-block;
vertical-align: middle;
font-size: 1rem;
}
}
// Generate `:hover` and `:focus` styles in one go.
//
// @link https://github.com/inuitcss/inuitcss/blob/master/tools/_tools.mixins.scss
// @content Wrapped in `:focus` and `:hover` pseudo-classes.
// @output Wraps the given content in `:focus` and `:hover` pseudo-classes.
@mixin u-hocus {
&:focus,
&:hover {
@content;
}
}
// Generate `:active` and `:focus` styles in one go.
//
// @see {Mixin} u-hocus
// @content Wrapped in `:focus` and `:active` pseudo-classes.
// @output Wraps the given content in `:focus` and `:hover` pseudo-classes.
@mixin u-actus {
&:focus,
&:active {
@content;
}
}
// Prevent text from wrapping onto multiple lines for the current element.
//
// An ellipsis is appended to the end of the line.
//
// 1. Ensure that the node has a maximum width after which truncation can occur.
// 2. Fix for IE 8/9 if `word-wrap: break-word` is in effect on ancestor nodes.
//
// @param {Number} $width [100%] - The maximum width of element.
// @output `max-width`, `word-wrap`, `white-space`, `overflow`, `text-overflow`
@mixin u-truncate($width: 100%) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal; // [2]
@if $width {
max-width: $width; // [1]
}
}
// Applies accessible hiding to the current element.
//
// @param {Boolean} $important [true] - Whether the visibility is important.
// @output Properties for removing the element from the document flow.
@mixin u-accessibly-hidden($important: true) {
$important: important($important);
position: absolute $important;
overflow: hidden;
clip: rect(0 0 0 0);
margin: 0;
padding: 0;
width: 1px;
height: 1px;
border: 0;
}
// Allows an accessibly hidden element to be focusable via keyboard navigation.
//
// @content For styling the now visible element.
// @output Injects `:focus`, `:active` pseudo-classes.
@mixin u-accessibly-focusable {
@include u-actus {
clip: auto;
width: auto;
height: auto;
@content;
}
}
// Hide the current element from all.
//
// The element will be hidden from screen readers and removed from the document flow.
//
// @link http://juicystudio.com/article/screen-readers-display-none.php
// @param {Boolean} $important [true] - Whether the visibility is important.
// @output `display`, `visibility`
@mixin u-hidden($important: true) {
$important: important($important);
display: none $important;
visibility: hidden $important;
}
// Show the current element for all.
//
// The element will be accessible from screen readers and visible in the document flow.
//
// @param {String} $display [block] - The rendering box used for the element.
// @param {Boolean} $important [true] - Whether the visibility is important.
// @output `display`, `visibility`
@mixin u-shown($display: block, $important: true) {
$important: important($important);
display: $display $important;
visibility: visible $important;
}
// Aspect-ratio polyfill
//
// @param {Number} $ratio [19/6] - The ratio of the element.
// @param {Number} $width [100%] - The fallback width of element.
// @param {Boolean} $children [false] - Whether the element contains children for the fallback properties.
// @output Properties for maintaining aspect-ratio
@mixin aspect-ratio($ratio: math.div(16, 9), $width: 100%, $children: false) {
@supports (aspect-ratio: 1) {
aspect-ratio: $ratio;
}
@supports not (aspect-ratio: 1) {
height: 0;
padding-top: calc(#{$width} * #{math.div(1, $ratio)});
@if ($children == true) {
position: relative;
> * {
position: absolute;
top: 0;
left: 0;
}
}
}
}
// Add focus state to focused element
@mixin u-focus-visible (
$color: currentColor,
$style: solid,
$width: 2px,
$offset: 2px,
) {
outline-color: $color;
outline-style: $style;
outline-width: $width;
outline-offset: $offset;
}
================================================
FILE: packages/landing/assets/styles/tools/_widths.scss
================================================
// ==========================================================================
// Tools / Widths
// ==========================================================================
@use "sass:math";
@use "functions" as *;
// Optionally, the boilerplate can generate classes to offset items by a
// certain width. Would you like to generate these types of class as well? E.g.:
//
// @example css
// .u-push-1/3
// .u-pull-2/4
// .u-pull-1/5
// .u-push-2/3
$widths-offsets: false !default;
// By default, the boilerplate uses fractions-like classes like `
`.
// You can change the `/` to whatever you fancy with this variable.
$fractions-delimiter: \/ !default;
// When using Sass-MQ, this defines the separator for the breakpoints suffix
// in the class name. By default, we are generating the responsive suffixes
// for the classes with a `@` symbol so you get classes like:
//
$breakpoint-delimiter: \@ !default;
// Generate a series of width helper classes
//
// @example scss
// @include widths(12);
//
// @example html
//
//
// @link https://github.com/inuitcss/inuitcss/commit/6eb574f/utilities/_utilities.widths.scss
// @requires {Function} important
// @requires {Function} $widths-offsets
// @requires {Function} $fractions-delimiter
// @requires {Function} $breakpoint-delimiter
// @param {List} $colums - The columns we want the widths to have.
// @param {String} $breakpoint - Optional suffix for responsive widths.
// @output `width`, `position`, `right`, `left`
@mixin widths($columns, $breakpoint: null, $important: true) {
$important: important($important);
// Loop through the number of columns for each denominator of our fractions.
@each $denominator in $columns {
// Begin creating a numerator for our fraction up until we hit the
// denominator.
@for $numerator from 1 through $denominator {
// Build a class in the format `.u-3/4[@]`.
.u-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
width: math.div($numerator, $denominator) * 100% $important;
}
@if ($widths-offsets == true) {
// Build a class in the format `.u-push-1/2[@]`.
.u-push-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
position: relative $important;
right: auto $important;
left: math.div($numerator, $denominator) * 100% $important;
}
// Build a class in the format `.u-pull-5/6[@]`.
.u-pull-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
position: relative $important;
right: math.div($numerator, $denominator) * 100% $important;
left: auto $important;
}
}
}
}
}
================================================
FILE: packages/landing/assets/styles/utilities/_align.scss
================================================
// ==========================================================================
// Utilities / Alignment
// ==========================================================================
// Floats
// ==========================================================================
.u-float-left {
float: left !important;
}
.u-float-right {
float: right !important;
}
// Horizontal Text
// ==========================================================================
.u-text-center {
text-align: center !important;
}
.u-text-left {
text-align: left !important;
}
.u-text-right {
text-align: right !important;
}
// Vertical Text
// ==========================================================================
.u-align-baseline {
vertical-align: baseline !important;
}
.u-align-bottom {
vertical-align: bottom !important;
}
.u-align-middle {
vertical-align: middle !important;
}
.u-align-top {
vertical-align: top !important;
}
.u-vertical-center {
@include o-vertical-center;
}
================================================
FILE: packages/landing/assets/styles/utilities/_grid-column.scss
================================================
// ==========================================================================
// Tools / Grid Columns
// ==========================================================================
@use "../core" as *;
//
// Grid layout system.
//
// This tool generates columns for all needed media queries.
// Unused classes will be purge by the css post-processor.
//
$colsMax: $base-column-nb + 1;
@each $breakpoint, $mediaquery in $breakpoints {
@for $fromIndex from 1 through $colsMax {
@for $toIndex from 1 through $colsMax {
// Columns without media query
@if $breakpoint == "tiny" {
.u-gc-#{$fromIndex}\/#{$toIndex} {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
// Columns min-width breakpoints `@from-*`
.u-gc-#{$fromIndex}\/#{$toIndex}\@from-#{$breakpoint} {
@media #{mq-min($breakpoint)} {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
// Columns max-width breakpoints @to-*`
.u-gc-#{$fromIndex}\/#{$toIndex}\@to-#{$breakpoint} {
@media #{mq-max($breakpoint)} {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
}
}
}
================================================
FILE: packages/landing/assets/styles/utilities/_helpers.scss
================================================
// ==========================================================================
// Utilities / Helpers
// ==========================================================================
// Layout
// ==========================================================================
@use '../core' as *;
.u-relative {
position: relative;
}
.u-clipped {
clip-path: polygon(0% 0, 100% 00%, 100% 100%, 0 100%);
}
.u-max {
&-w300 {
max-width: rem(300px);
}
&-w440 {
max-width: rem(440px);
}
}
.u-glyph {
font-family: ff('serif');
font-feature-settings: 'dlig' on, 'ss01' on, 'salt' on;
font-weight: $font-weight-regular;
}
.u-hover-underline {
position: relative;
@media (hover: hover) {
&::before {
content: '';
position: absolute;
bottom: -0.1em;
left: 0;
width: 100%;
height: 1px;
background-color: currentColor;
transform: scale3d(0, 1, 1);
transition: transform t(fast) ease('power3.out');
transform-origin: top right;
}
&:hover,
.u-hover:hover & {
&::before {
transform: scale3d(1, 1, 1);
transform-origin: top left;
}
}
}
}
// Completely remove from the flow but leave available to screen readers.
.u-screen-reader-text {
@include u-accessibly-hidden;
}
@media not print {
.u-screen-reader-text\@screen {
@include u-accessibly-hidden;
}
}
// Extends the `.screen-reader-text` class to allow the element
// to be focusable when navigated to via the keyboard.
//
// @link https://www.drupal.org/node/897638
// @todo Define styles when focused.
.u-screen-reader-text.-focusable {
@include u-accessibly-focusable;
}
.u-external-icon {
font-size: 0.85em;
}
.u-text-balance {
text-wrap: balance;
}
.u-hidden-md {
@media (max-width: $to-medium) {
display: none;
}
}
================================================
FILE: packages/landing/assets/styles/utilities/_print.scss
================================================
// ==========================================================================
// Utilities / Print Mode
// ==========================================================================
////
/// Very crude, reset-like styles taken from the HTML5 Boilerplate:
/// - https://github.com/h5bp/html5-boilerplate/blob/5.3.0/dist/doc/css.md#print-styles
/// - https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css#L205-L282
///
/// @link https://github.com/inuitcss/inuitcss/blob/c27993f/utilities/_utilities.print.scss
////
@media print {
// 1. Black prints faster: http://www.sanbeiji.com/archives/953
*,
*:before,
*:after,
*:first-letter,
*:first-line {
background: transparent !important;
box-shadow: none !important;
color: #000000 !important; // [1]
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
// Don't show links that are fragment identifiers, or use the `javascript:`
// pseudo protocol.
a[href^="#"]:after,
a[href^="javascript:"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999999;
page-break-inside: avoid;
}
// Printing Tables: http://css-discuss.incutio.com/wiki/Printing_Tables
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
================================================
FILE: packages/landing/assets/styles/utilities/_ratio.scss
================================================
// ==========================================================================
// Utilities / Ratio
// ==========================================================================
@use "sass:meta";
// @link https://github.com/inuitcss/inuitcss/blob/19d0c7e/objects/_objects.ratio.scss
// A list of aspect ratios that get generated as modifier classes.
$aspect-ratios: (
(2:1),
(4:3),
(16:9),
) !default;
/* stylelint-disable */
// Generate a series of ratio classes to be used like so:
//
// @example
//
@each $ratio in $aspect-ratios {
@each $antecedent, $consequent in $ratio {
@if (meta.type-of($antecedent) != number) {
@error "`#{$antecedent}` needs to be a number."
}
@if (meta.type-of($consequent) != number) {
@error "`#{$consequent}` needs to be a number."
}
.u-#{$antecedent}\:#{$consequent}::before {
padding-bottom: math.div($consequent, $antecedent) * 100%;
}
}
}
/* stylelint-enable */
================================================
FILE: packages/landing/assets/styles/utilities/_spacing.scss
================================================
// ==========================================================================
// Utilities / Spacing
// ==========================================================================
@use "../core" as *;
@use "sass:list";
////
/// Utility classes to put specific spacing values onto elements. The below loop
/// will generate us a suite of classes like:
///
/// @example
/// .u-margin-top {}
/// .u-padding-left-large {}
/// .u-margin-right-small {}
/// .u-padding {}
/// .u-padding-right-none {}
/// .u-padding-horizontal {}
/// .u-padding-vertical-small {}
///
/// @link https://github.com/inuitcss/inuitcss/blob/512977a/utilities/_utilities.spacing.scss
////
/* stylelint-disable string-quotes */
$spacing-directions: (
null: null,
'-top': '-top',
'-right': '-right',
'-bottom': '-bottom',
'-left': '-left',
'-x': '-left' '-right',
'-y': '-top' '-bottom',
) !default;
$spacing-properties: (
'padding': 'padding',
'margin': 'margin',
) !default;
$spacing-sizes: list.join($spacers, (
null: var(--grid-gutter),
'none': 0
));
@each $breakpoint, $mediaquery in $breakpoints {
@each $property-namespace, $property in $spacing-properties {
@each $direction-namespace, $directions in $spacing-directions {
@each $size-namespace, $size in $spacing-sizes {
// Prepend "-" to spacing sizes if not null
@if ($size-namespace != null) {
$size-namespace: "-" + $size-namespace;
}
// Base class
$base-class: ".u-" + #{$property-namespace}#{$direction-namespace}#{$size-namespace};
// Spacer without media query
@if $breakpoint == "tiny" {
#{$base-class} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
// Spacer min-width breakpoints `@from-*`
#{$base-class}\@from-#{$breakpoint} {
@media #{mq-min($breakpoint)} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
// Spacer max-width breakpoints @to-*`
#{$base-class}\@to-#{$breakpoint} {
@media #{mq-max($breakpoint)} {
@each $direction in $directions {
#{$property}#{$direction}: $size !important;
}
}
}
}
}
}
}
/* stylelint-enable string-quotes */
================================================
FILE: packages/landing/assets/styles/utilities/_states.scss
================================================
// ==========================================================================
// Utilities / States
// ==========================================================================
// ARIA roles display visual cursor hints
[aria-busy="true"] {
cursor: progress;
}
[aria-controls] {
cursor: pointer;
}
[aria-disabled] {
cursor: default;
}
// Control visibility without affecting flow.
.is-visible {
visibility: visible !important;
opacity: 1 !important;
}
.is-invisible {
visibility: hidden !important;
opacity: 0 !important;
}
// Completely remove from the flow and screen readers.
.is-hidden {
@include u-hidden;
}
@media not print {
.is-hidden\@screen {
@include u-hidden;
}
}
@media print {
.is-hidden\@print {
@include u-hidden;
}
}
// .is-hidden\@to-large {
// @media (max-width: $to-large) {
// display: none;
// }
// }
//
// .is-hidden\@from-large {
// @media (min-width: $from-large) {
// display: none;
// }
// }
// // Display a hidden-by-default element.
//
// .is-shown {
// @include u-shown;
// }
//
// table.is-shown {
// display: table !important;
// }
//
// tr.is-shown {
// display: table-row !important;
// }
//
// td.is-shown,
// th.is-shown {
// display: table-cell !important;
// }
================================================
FILE: packages/landing/assets/styles/utilities/_theme.scss
================================================
// ==========================================================================
// Utilities / Theme
// ==========================================================================
@use "../core" as *;
:root {
--color-text: #{color(blue)};
--color-background: #{color(white)};
}
[data-theme="white"] {
color: var(--color-text);
background-color: var(--color-background);
}
[data-theme="blue"] {
--color-text: #{color(white)};
--color-background: #{color(blue)};
color: var(--color-text);
background-color: var(--color-background);
}
[data-theme="black"] {
--color-text: #{color(white)};
--color-background: #{color(black)};
color: var(--color-text);
background-color: var(--color-background);
}
================================================
FILE: packages/landing/assets/styles/utilities/_widths.scss
================================================
// ==========================================================================
// Utilities / Widths
// ==========================================================================
////
/// @link https://github.com/inuitcss/inuitcss/blob/6eb574f/utilities/_utilities.widths.scss
///
///
/// Which fractions would you like in your grid system(s)?
/// By default, the boilerplate provides fractions of one whole, halves, thirds,
/// quarters, and fifths, e.g.:
///
/// @example css
/// .u-1/2
/// .u-2/5
/// .u-3/4
/// .u-2/3
////
$widths-fractions: 1 2 3 4 5 !default;
@include widths($widths-fractions);
.u-1\/2\@from-small {
@media (min-width: $from-small) {
width: span(1/2);
}
}
================================================
FILE: packages/landing/assets/styles/vendors/.gitkeep
================================================
================================================
FILE: packages/landing/assets.json
================================================
{
"version": 1767911710101
}
================================================
FILE: packages/landing/build/build.js
================================================
import buildEleventy from './tasks/eleventy.js';
import concatFiles from './tasks/concats.js';
import compileScripts from './tasks/scripts.js';
import compileStyles from './tasks/styles.js';
import compileSVGs from './tasks/svgs.js';
import bumpVersions from './tasks/versions.js';
buildEleventy();
concatFiles();
compileScripts();
compileStyles();
compileSVGs();
bumpVersions();
================================================
FILE: packages/landing/build/helpers/config.js
================================================
/**
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json' with { type: 'json' };
import { merge } from '../utils/index.js';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json', {
with: { type: 'json' },
});
usrconfig = usrconfig.default;
merge(loconfig, usrconfig);
} catch (err) {
// do nothing
}
export default loconfig;
export {
loconfig,
};
================================================
FILE: packages/landing/build/helpers/glob.js
================================================
/**
* @file Retrieve the first available glob library.
*
* Note that options vary between libraries.
*
* Candidates:
*
* - {@link https://npmjs.com/package/tiny-glob tiny-glob} [1][5][6]
* - {@link https://npmjs.com/package/globby globby} [2][5]
* - {@link https://npmjs.com/package/fast-glob fast-glob} [3]
* - {@link https://npmjs.com/package/glob glob} [1][4][5]
*
* Notes:
*
* - [1] The library's function accepts only a single pattern.
* - [2] The library's function accepts only an array of patterns.
* - [3] The library's function accepts either a single pattern
* or an array of patterns.
* - [4] The library's function does not return a Promise but will be
* wrapped in a function that does return a Promise.
* - [5] The library's function will be wrapped in a function that
* supports a single pattern and an array of patterns.
* - [6] The library's function returns files and directories but will be
* preconfigured to return only files.
*/
import { promisify } from 'node:util';
/**
* @callback GlobFn
*
* @param {string|string[]} patterns - A string pattern
* or an array of string patterns.
* @param {object} options
*
* @returns {Promise}
*/
/**
* @typedef {object} GlobOptions
*/
/**
* @type {GlobFn|undefined} The discovered glob function.
*/
let glob;
/**
* @type {string[]} A list of packages to attempt import.
*/
const candidates = [
'tiny-glob',
'globby',
'fast-glob',
'glob',
];
try {
glob = await importGlob();
} catch (err) {
// do nothing
}
/**
* @type {boolean} Whether a glob function was discovered (TRUE) or not (FALSE).
*/
const supportsGlob = (typeof glob === 'function');
/**
* Imports the first available glob function.
*
* @throws {TypeError} If no glob library was found.
*
* @returns {GlobFn}
*/
async function importGlob() {
for (let name of candidates) {
try {
let globModule = await import(name);
if (typeof globModule.default !== 'function') {
throw new TypeError(`Expected ${name} to be a function`);
}
/**
* Wrap the function to ensure
* a common pattern.
*/
switch (name) {
case 'tiny-glob':
/** [1][5] */
return createArrayableGlob(
/** [6] */
createPresetGlob(globModule.default, {
filesOnly: true
})
);
case 'globby':
/** [2][5] - If `patterns` is a string, wraps into an array. */
return (patterns, options) => globModule.default([].concat(patterns), options);
case 'glob':
/** [1][5] */
return createArrayableGlob(
/** [4] */
promisify(globModule.default)
);
default:
return globModule.default;
}
} catch (err) {
// swallow this error; skip to the next candidate.
}
}
throw new TypeError(
`No glob library was found, expected one of: ${candidates.join(', ')}`
);
}
/**
* Creates a wrapper function for the glob function
* to provide support for arrays of patterns.
*
* @param {function} globFn - The glob function.
*
* @returns {GlobFn}
*/
function createArrayableGlob(globFn) {
return (patterns, options) => {
/** [2] If `patterns` is a string, wraps into an array. */
patterns = [].concat(patterns);
const globs = patterns.map((pattern) => globFn(pattern, options));
return Promise.all(globs).then((files) => {
return [].concat.apply([], files);
});
};
}
/**
* Creates a wrapper function for the glob function
* to define new default options.
*
* @param {function} globFn - The glob function.
* @param {GlobOptions} presets - The glob function options to preset.
*
* @returns {GlobFn}
*/
function createPresetGlob(globFn, presets) {
return (patterns, options) => globFn(patterns, Object.assign({}, presets, options));
}
export default glob;
export {
glob,
supportsGlob,
};
================================================
FILE: packages/landing/build/helpers/message.js
================================================
/**
* @file Provides a decorator for console messages.
*/
import kleur from 'kleur';
/**
* Outputs a message to the console.
*
* @param {string} text - The message to output.
* @param {string} [type] - The type of message.
* @param {string} [timerID] - The console time label to output.
*/
function message(text, type, timerID) {
switch (type) {
case 'success':
console.log('✅ ', kleur.bgGreen().black(text));
break;
case 'chore':
console.log('🧹 ', kleur.bgGreen().black(text));
break;
case 'notice':
console.log('ℹ️ ', kleur.bgBlue().black(text));
break;
case 'error':
console.log('❌ ', kleur.bgRed().black(text));
break;
case 'warning':
console.log('⚠️ ', kleur.bgYellow().black(text));
break;
case 'waiting':
console.log('⏱ ', kleur.blue().italic(text));
if (timerID != null) {
console.timeLog(timerID);
timerID = null;
}
break;
default:
console.log(text);
break;
}
if (timerID != null) {
console.timeEnd(timerID);
}
console.log('');
}
export default message;
export {
message,
};
================================================
FILE: packages/landing/build/helpers/notification.js
================================================
/**
* @file Provides a decorator for cross-platform notification.
*/
import notifier from 'node-notifier';
/**
* Sends a cross-platform native notification.
*
* Wraps around node-notifier to assign default values.
*
* @param {string|object} options - The notification options or a message.
* @param {string} options.title - The notification title.
* @param {string} options.message - The notification message.
* @param {string} options.icon - The notification icon.
* @param {function} callback - The notification callback.
* @return {void}
*/
function notification(options, callback) {
if (typeof options === 'string') {
options = {
message: options
};
} else if (!options.title && !options.message) {
throw new TypeError(
'Notification expects at least a \'message\' parameter'
);
}
if (typeof options.icon === 'undefined') {
options.icon = 'https://user-images.githubusercontent.com/4596862/54868065-c2aea200-4d5e-11e9-9ce3-e0013c15f48c.png';
}
// If notification does not use a callback,
// shorten the wait before timing out.
if (typeof callback === 'undefined') {
if (typeof options.wait === 'undefined') {
if (typeof options.timeout === 'undefined') {
options.timeout = 5;
}
}
}
notifier.notify(options, callback);
}
export default notification;
export {
notification,
};
================================================
FILE: packages/landing/build/helpers/postcss.js
================================================
/**
* @file If available, returns the PostCSS Processor creator and
* any the Autoprefixer PostCSS plugin.
*/
/**
* @typedef {import('autoprefixer').autoprefixer.Options} AutoprefixerOptions
*/
/**
* @typedef {import('postcss').AcceptedPlugin} AcceptedPlugin
*/
/**
* @typedef {import('postcss').Postcss} Postcss
*/
/**
* @typedef {import('postcss').ProcessOptions} ProcessOptions
*/
/**
* @typedef {import('postcss').Processor} Processor
*/
/**
* @typedef {AcceptedPlugin[]} PluginList
*/
/**
* @typedef {object} PluginMap
*/
/**
* @typedef {PluginList|PluginMap} PluginCollection
*/
/**
* @typedef {object} PostCSSOptions
*
* @property {ProcessOptions} processor - The `Processor#process()` options.
* @property {AutoprefixerOptions} autoprefixer - The `autoprefixer()` options.
*/
/**
* @type {Postcss|undefined} postcss - The discovered PostCSS function.
* @type {AcceptedPlugin|undefined} autoprefixer - The discovered Autoprefixer function.
*/
let postcss, autoprefixer;
try {
postcss = await import('postcss');
postcss = postcss.default;
autoprefixer = await import('autoprefixer');
autoprefixer = autoprefixer.default;
} catch (err) {
// do nothing
}
/**
* @type {boolean} Whether PostCSS was discovered (TRUE) or not (FALSE).
*/
const supportsPostCSS = (typeof postcss === 'function');
/**
* @type {PluginList} A list of supported plugins.
*/
const pluginsList = [
autoprefixer,
];
/**
* @type {PluginMap} A map of supported plugins.
*/
const pluginsMap = {
'autoprefixer': autoprefixer,
};
/**
* Attempts to create a PostCSS Processor with the given plugins and options.
*
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {PostCSSOptions} options - The PostCSS wrapper options.
*
* @returns {Processor|null}
*/
function createProcessor(pluginsListOrMap, options)
{
if (!postcss) {
return null;
}
const plugins = parsePlugins(pluginsListOrMap, options);
return postcss(plugins);
}
/**
* Parses the PostCSS plugins and options.
*
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {PostCSSOptions} options - The PostCSS wrapper options.
*
* @returns {PluginList}
*/
function parsePlugins(pluginsListOrMap, options)
{
if (Array.isArray(pluginsListOrMap)) {
return pluginsListOrMap;
}
/** @type {PluginList} */
const plugins = [];
for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) {
if (name in options) {
plugin = plugin[name](options[name]);
}
plugins.push(plugin);
}
return plugins;
}
export default postcss;
export {
autoprefixer,
createProcessor,
parsePlugins,
pluginsList,
pluginsMap,
postcss,
supportsPostCSS,
};
================================================
FILE: packages/landing/build/helpers/template.js
================================================
/**
* @file Provides simple template tags.
*/
import loconfig from './config.js';
import {
escapeRegExp,
flatten
} from '../utils/index.js';
const templateData = flatten({
paths: loconfig.paths
});
/**
* Replaces all template tags from a map of keys and values.
*
* If replacement pairs contain a mix of substrings, regular expressions,
* and functions, regular expressions are executed last.
*
* @param {*} input - The value being searched and replaced on.
* If input is, or contains, a string, tags will be resolved.
* If input is, or contains, an object, it is mutated directly.
* If input is, or contains, an array, a shallow copy is returned.
* Otherwise, the value is left intact.
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {*} Returns the transformed value.
*/
function resolve(input, data = templateData) {
switch (typeof input) {
case 'string': {
return resolveValue(input, data);
}
case 'object': {
if (input == null) {
break;
}
if (Array.isArray(input)) {
return input.map((value) => resolve(value, data));
} else {
for (const key in input) {
input[key] = resolve(input[key], data);
}
}
}
}
return input;
}
/**
* Replaces all template tags in a string from a map of keys and values.
*
* If replacement pairs contain a mix of substrings, regular expressions,
* and functions, regular expressions are executed last.
*
* @param {string} input - The string being searched and replaced on.
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {string} Returns the translated string.
*/
function resolveValue(input, data = templateData) {
const tags = [];
if (data !== templateData) {
data = flatten(data);
}
for (let tag in data) {
tags.push(escapeRegExp(tag));
}
if (tags.length === 0) {
return input;
}
const search = new RegExp('\\{%\\s*(' + tags.join('|') + ')\\s*%\\}', 'g');
return input.replace(search, (match, key) => {
let value = data[key];
switch (typeof value) {
case 'function':
/**
* Retrieve the offset of the matched substring `args[0]`
* and the whole string being examined `args[1]`.
*/
let args = Array.prototype.slice.call(arguments, -2);
return value.call(data, match, args[0], args[1]);
case 'string':
case 'number':
return value;
}
return '';
});
}
export default resolve;
export {
resolve,
resolveValue,
};
================================================
FILE: packages/landing/build/migrate_imports.js
================================================
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const files = [
'generic/_generic.scss',
'generic/_media.scss',
'generic/_button.scss',
'elements/_document.scss',
'objects/_container.scss',
'objects/_icons.scss',
'objects/_grid.scss',
'components/_heading.scss',
'components/_text.scss',
'components/_button.scss',
'components/_form.scss',
'components/_header.scss',
'components/_hero.scss',
'components/_rail.scss',
'components/_cascade.scss',
'components/_section-heading.scss',
'components/_tool.scss',
'components/_features-grid.scss',
'components/_sticky-heading.scss',
'components/_list.scss',
'components/_footer.scss',
'components/_fadeInText.scss',
'components/_preloader.scss',
'utilities/_grid-column.scss',
'utilities/_theme.scss',
'utilities/_helpers.scss',
'utilities/_spacing.scss'
];
const basePath = '/Users/arnvvd/Projects/locomotive-scroll/packages/landing/assets/styles';
files.forEach(file => {
const filePath = path.join(basePath, file);
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf8');
// Check if already has @use
if (content.includes('@use "../core"')) {
console.log(`Skipping ${file}, already has @use`);
return;
}
// Try to find the header block
const headerRegex = /^(\/\/ =+[\r\n]+(?:\/\/.*[\r\n]+)*\/\/ =+[\r\n]+)/;
const match = content.match(headerRegex);
if (match) {
// Insert after header
const header = match[1];
const newContent = content.replace(header, header + '\n@use "../core" as *;\n');
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`Updated ${file}`);
} else {
// Prepend to top
const newContent = '@use "../core" as *;\n\n' + content;
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`Updated ${file} (prepended)`);
}
} else {
console.error(`File not found: ${filePath}`);
}
});
================================================
FILE: packages/landing/build/tasks/concats.js
================================================
import loconfig from '../helpers/config.js';
import glob, { supportsGlob } from '../helpers/glob.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import concat from 'concat';
import {
basename,
normalize,
} from 'node:path';
/**
* @const {object} defaultGlobOptions - The default shared glob options.
* @const {object} developmentGlobOptions - The predefined glob options for development.
* @const {object} productionGlobOptions - The predefined glob options for production.
*/
export const defaultGlobOptions = {
};
export const developmentGlobOptions = Object.assign({}, defaultGlobOptions);
export const productionGlobOptions = Object.assign({}, defaultGlobOptions);
/**
* @typedef {object} ConcatOptions
* @property {boolean} removeDuplicates - Removes duplicate paths from
* the array of matching files and folders.
* Only the first occurrence of each path is kept.
*/
/**
* @const {ConcatOptions} defaultConcatOptions - The default shared concatenation options.
* @const {ConcatOptions} developmentConcatOptions - The predefined concatenation options for development.
* @const {ConcatOptions} productionConcatOptions - The predefined concatenation options for production.
*/
export const defaultConcatOptions = {
removeDuplicates: true,
};
export const developmentConcatOptions = Object.assign({}, defaultConcatOptions);
export const productionConcatOptions = Object.assign({}, defaultConcatOptions);
/**
* @const {object} developmentConcatFilesArgs - The predefined `concatFiles()` options for development.
* @const {object} productionConcatFilesArgs - The predefined `concatFiles()` options for production.
*/
export const developmentConcatFilesArgs = [
developmentGlobOptions,
developmentConcatOptions,
];
export const productionConcatFilesArgs = [
productionGlobOptions,
productionConcatOptions,
];
/**
* Concatenates groups of files.
*
* @todo Add support for minification.
*
* @async
* @param {object|boolean} [globOptions=null] - Customize the glob options.
* If `null`, default production options are used.
* If `false`, the glob function will be ignored.
* @param {object} [concatOptions=null] - Customize the concatenation options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function concatFiles(globOptions = null, concatOptions = null) {
if (supportsGlob) {
if (globOptions == null) {
globOptions = productionGlobOptions;
} else if (
globOptions !== false &&
globOptions !== developmentGlobOptions &&
globOptions !== productionGlobOptions
) {
globOptions = merge({}, defaultGlobOptions, globOptions);
}
}
if (concatOptions == null) {
concatOptions = productionConcatOptions;
} else if (
concatOptions !== developmentConcatOptions &&
concatOptions !== productionConcatOptions
) {
concatOptions = merge({}, defaultConcatOptions, concatOptions);
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.includes - One or more paths to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @return {Promise}
*/
loconfig.tasks.concats?.forEach(async ({
includes,
outfile,
label = null
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
const timeLabel = `${label} concatenated in`;
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
outfile = resolve(outfile);
if (supportsGlob && globOptions) {
includes = await glob(includes, globOptions);
}
if (concatOptions.removeDuplicates) {
includes = includes.map((path) => normalize(path));
includes = [ ...new Set(includes) ];
}
await concat(includes, outfile);
if (includes.length) {
message(`${label} concatenated`, 'success', timeLabel);
} else {
message(`${label} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error concatenating ${label}`, 'error');
message(err);
notification({
title: `${label} concatenation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
});
};
================================================
FILE: packages/landing/build/tasks/eleventy.js
================================================
import message from '../helpers/message.js';
import { merge } from '../utils/index.js';
import Eleventy from "@11ty/eleventy";
export const defaultEleventyOptions = {
production: true
}
export const developmentEleventyOptions = {
production: false
}
export const productionEleventyOptions = {
production: true
}
let elev;
export default async function buildEleventy(eleventyOptions = null) {
if (eleventyOptions == null) {
eleventyOptions = productionEleventyOptions;
} else if (
eleventyOptions !== developmentEleventyOptions &&
eleventyOptions !== productionEleventyOptions
) {
eleventyOptions = merge({}, defaultEleventyOptions, eleventyOptions);
}
const { production } = eleventyOptions;
const timeLabel = `11ty compiled in`;
console.time(timeLabel);
try {
if(!elev) {
elev = new Eleventy();
if(!production) {
await elev.watch();
}
}
// Disable caching to ensure a fresh build each time
await elev.write();
message(`11ty compiled`, 'success', timeLabel);
} catch(err) {
console.error(err)
message(err, 'error', timeLabel);
}
}
================================================
FILE: packages/landing/build/tasks/scripts.js
================================================
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import esbuild from 'esbuild';
import { basename } from 'node:path';
/**
* @const {object} defaultESBuildOptions - The default shared ESBuild options.
* @const {object} developmentESBuildOptions - The predefined ESBuild options for development.
* @const {object} productionESBuildOptions - The predefined ESBuild options for production.
*/
export const defaultESBuildOptions = {
bundle: true,
color: true,
sourcemap: true,
target: [
'es2015',
],
};
export const developmentESBuildOptions = Object.assign({}, defaultESBuildOptions);
export const productionESBuildOptions = Object.assign({}, defaultESBuildOptions, {
logLevel: 'warning',
minify: true,
});
/**
* @const {object} developmentScriptsArgs - The predefined `compileScripts()` options for development.
* @const {object} productionScriptsArgs - The predefined `compileScripts()` options for production.
*/
export const developmentScriptsArgs = [
developmentESBuildOptions,
];
export const productionScriptsArgs = [
productionESBuildOptions,
];
/**
* Bundles and minifies main JavaScript files.
*
* @async
* @param {object} [esBuildOptions=null] - Customize the ESBuild build API options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function compileScripts(esBuildOptions = null) {
if (esBuildOptions == null) {
esBuildOptions = productionESBuildOptions;
} else if (
esBuildOptions !== developmentESBuildOptions &&
esBuildOptions !== productionESBuildOptions
) {
esBuildOptions = merge({}, defaultESBuildOptions, esBuildOptions);
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.includes - One or more paths to process.
* @param {string} [entry.outdir] - The directory to write to.
* @param {string} [entry.outfile] - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outdir or outfile name.
* @throws {TypeError} If outdir and outfile are missing.
* @return {Promise}
*/
loconfig.tasks.scripts?.forEach(async ({
includes,
outdir = '',
outfile = '',
label = null
}) => {
if (!label) {
label = basename(outdir || outfile || 'undefined');
}
const timeLabel = `${label} compiled in`;
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
if (outdir) {
outdir = resolve(outdir);
} else if (outfile) {
outfile = resolve(outfile);
} else {
throw new TypeError(
'Expected \'outdir\' or \'outfile\''
);
}
await esbuild.build(Object.assign({}, esBuildOptions, {
entryPoints: includes,
outdir,
outfile,
}));
message(`${label} compiled`, 'success', timeLabel);
} catch (err) {
// errors managments (already done in esbuild)
notification({
title: `${label} compilation failed 🚨`,
message: `${err.errors[0].text} in ${err.errors[0].location.file} line ${err.errors[0].location.line}`
});
}
});
};
================================================
FILE: packages/landing/build/tasks/styles.js
================================================
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import {
createProcessor,
pluginsMap as postcssPluginsMap,
supportsPostCSS
} from '../helpers/postcss.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { promisify } from 'node:util';
import * as sass from 'sass';
import { PurgeCSS } from 'purgecss';
const sassRender = promisify(sass.render);
let postcssProcessor;
/**
* @const {object} defaultSassOptions - The default shared Sass options.
* @const {object} developmentSassOptions - The predefined Sass options for development.
* @const {object} productionSassOptions - The predefined Sass options for production.
*/
export const defaultSassOptions = {
omitSourceMapUrl: true,
sourceMap: true,
sourceMapContents: true,
};
export const developmentSassOptions = Object.assign({}, defaultSassOptions, {
outputStyle: 'expanded',
});
export const productionSassOptions = Object.assign({}, defaultSassOptions, {
outputStyle: 'compressed',
});
/**
* @const {object} defaultPostCSSOptions - The default shared PostCSS options.
* @const {object} developmentPostCSSOptions - The predefined PostCSS options for development.
* @const {object} productionPostCSSOptions - The predefined PostCSS options for production.
*/
export const defaultPostCSSOptions = {
processor: {
map: {
annotation: false,
inline: false,
sourcesContent: true,
},
},
};
export const developmentPostCSSOptions = Object.assign({}, defaultPostCSSOptions);
export const productionPostCSSOptions = Object.assign({}, defaultPostCSSOptions);
/**
* @const {object|boolean} developmentStylesArgs - The predefined `compileStyles()` options for development.
* @const {object|boolean} productionStylesArgs - The predefined `compileStyles()` options for production.
*/
export const developmentStylesArgs = [
developmentSassOptions,
developmentPostCSSOptions,
false
];
export const productionStylesArgs = [
productionSassOptions,
productionPostCSSOptions,
true
];
/**
* Compiles and minifies main Sass files to CSS.
*
* @todo Add deep merge of `postcssOptions` to better support customization
* of default processor options.
*
* @async
* @param {object} [sassOptions=null] - Customize the Sass render API options.
* If `null`, default production options are used.
* @param {object|boolean} [postcssOptions=null] - Customize the PostCSS processor API options.
* If `null`, default production options are used.
* If `false`, PostCSS processing will be ignored.
* @return {Promise}
*/
export default async function compileStyles(sassOptions = null, postcssOptions = null, purge = true) {
if (sassOptions == null) {
sassOptions = productionSassOptions;
} else if (
sassOptions !== developmentSassOptions &&
sassOptions !== productionSassOptions
) {
sassOptions = merge({}, defaultSassOptions, sassOptions);
}
if (supportsPostCSS) {
if (postcssOptions == null) {
postcssOptions = productionPostCSSOptions;
} else if (
postcssOptions !== false &&
postcssOptions !== developmentPostCSSOptions &&
postcssOptions !== productionPostCSSOptions
) {
postcssOptions = merge({}, defaultPostCSSOptions, postcssOptions);
}
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.infile - The file to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @return {Promise}
*/
loconfig.tasks.styles?.forEach(async ({
infile,
outfile,
label = null
}) => {
const filestem = basename((outfile || 'undefined'), '.css');
const timeLabel = `${label || `${filestem}.css`} compiled in`;
console.time(timeLabel);
try {
infile = resolve(infile);
outfile = resolve(outfile);
let result = await sassRender(Object.assign({}, sassOptions, {
file: infile,
outFile: outfile,
}));
if (supportsPostCSS && postcssOptions) {
if (typeof postcssProcessor === 'undefined') {
postcssProcessor = createProcessor(
postcssPluginsMap,
postcssOptions
);
}
result = await postcssProcessor.process(
result.css,
Object.assign({}, postcssOptions.processor, {
from: outfile,
to: outfile,
})
);
if (result.warnings) {
const warnings = result.warnings();
if (warnings.length) {
message(`Error processing ${label || `${filestem}.css`}`, 'warning');
warnings.forEach((warn) => {
message(warn.toString());
});
}
}
}
try {
await writeFile(outfile, result.css).then(() => {
// Purge CSS once file exists.
if (outfile && purge) {
purgeUnusedCSS(outfile, `${label || `${filestem}.css`}`);
}
});
if (result.css) {
message(`${label || `${filestem}.css`} compiled`, 'success', timeLabel);
} else {
message(`${label || `${filestem}.css`} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error compiling ${label || `${filestem}.css`}`, 'error');
message(err);
notification({
title: `${label || `${filestem}.css`} save failed 🚨`,
message: `Could not save stylesheet to ${label || `${filestem}.css`}`
});
}
if (result.map) {
try {
await writeFile(outfile + '.map', result.map.toString());
} catch (err) {
message(`Error compiling ${label || `${filestem}.css.map`}`, 'error');
message(err);
notification({
title: `${label || `${filestem}.css.map`} save failed 🚨`,
message: `Could not save sourcemap to ${label || `${filestem}.css.map`}`
});
}
}
} catch (err) {
message(`Error compiling ${label || `${filestem}.scss`}`, 'error');
message(err.formatted || err);
notification({
title: `${label || `${filestem}.scss`} compilation failed 🚨`,
message: (err.formatted || `${err.name}: ${err.message}`)
});
}
});
};
/**
* Purge unused styles from CSS files.
*
* @async
*
* @param {string} outfile - The path of a css file
* If missing the function stops.
* @param {string} label - The CSS file label or name.
* @return {Promise}
*/
async function purgeUnusedCSS(outfile, label) {
const contentFiles = loconfig.tasks.purgeCSS?.content;
if (!Array.isArray(contentFiles) || !contentFiles.length) {
return;
}
label = label ?? basename(outfile);
const timeLabel = `${label} purged in`;
console.time(timeLabel);
const purgeCSSResults = await (new PurgeCSS()).purge({
content: contentFiles,
css: [ outfile ],
defaultExtractor: content => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [],
fontFaces: true,
keyframes: true,
safelist: {
// Keep all except .u-gc-* | .u-margin-* | .u-padding-*
standard: [ /^(?!.*\b(u-gc-|u-margin|u-padding)).*$/ ],
// Preserve CSS variables and their content in these selectors
deep: [ /fadeInText/ ]
},
// Disable variable purging to preserve all CSS custom properties
variables: false,
})
for (let result of purgeCSSResults) {
await writeFile(outfile, result.css)
message(`${label} purged`, 'chore', timeLabel);
}
}
================================================
FILE: packages/landing/build/tasks/svgs.js
================================================
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import notification from '../helpers/notification.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import { basename } from 'node:path';
import mixer from 'svg-mixer';
/**
* @const {object} defaultMixerOptions - The default shared Mixer options.
* @const {object} developmentMixerOptions - The predefined Mixer options for development.
* @const {object} productionMixerOptions - The predefined Mixer options for production.
*/
export const defaultMixerOptions = {
spriteConfig: {
usages: false,
},
};
export const developmentMixerOptions = Object.assign({}, defaultMixerOptions);
export const productionMixerOptions = Object.assign({}, defaultMixerOptions);
/**
* @const {object} developmentSVGsArgs - The predefined `compileSVGs()` options for development.
* @const {object} productionSVGsArgs - The predefined `compileSVGs()` options for production.
*/
export const developmentSVGsArgs = [
developmentMixerOptions,
];
export const productionSVGsArgs = [
productionMixerOptions,
];
/**
* Generates and transforms SVG spritesheets.
*
* @async
* @param {object} [mixerOptions=null] - Customize the Mixer API options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function compileSVGs(mixerOptions = null) {
if (mixerOptions == null) {
mixerOptions = productionMixerOptions;
} else if (
mixerOptions !== developmentMixerOptions &&
mixerOptions !== productionMixerOptions
) {
mixerOptions = merge({}, defaultMixerOptions, mixerOptions);
}
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string[]} entry.includes - One or more paths to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @return {Promise}
*/
loconfig.tasks.svgs?.forEach(async ({
includes,
outfile,
label = null
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
const timeLabel = `${label} compiled in`;
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
outfile = resolve(outfile);
const result = await mixer(includes, mixerOptions);
await result.write(outfile);
message(`${label} compiled`, 'success', timeLabel);
} catch (err) {
message(`Error compiling ${label}`, 'error');
message(err);
notification({
title: `${label} compilation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
});
};
================================================
FILE: packages/landing/build/tasks/versions.js
================================================
import loconfig from '../helpers/config.js';
import message from '../helpers/message.js';
import resolve from '../helpers/template.js';
import { merge } from '../utils/index.js';
import { randomBytes } from 'node:crypto';
import events from 'node:events';
import {
createReadStream,
createWriteStream,
} from 'node:fs';
import {
mkdir,
rename,
rm,
readFile,
writeFile,
} from 'node:fs/promises';
import {
basename,
dirname,
} from 'node:path';
import readline from 'node:readline';
export const REGEXP_SEMVER = /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
/**
* @typedef {object} VersionOptions
* @property {string|number|null} prettyPrint - A string or number to insert
* white space into the output JSON string for readability purposes.
* @property {string} versionFormat - The version number format.
* @property {string|RegExp} versionKey - Either:
* - A string representing the JSON field name assign the version number to.
*
* Explicit:
*
* ```json
* "key": "json:version"
* ```
*
* Implicit:
*
* ```json
* "key": "version"
* ```
*
* - A `RegExp` object or regular expression string prefixed with `regexp:`.
*
* ```json
* "key": "regexp:(?<=^const ASSETS_VERSION = ')(?\\d+)(?=';$)"
* ```
*
* ```js
* key: new RegExp('(?<=^const ASSETS_VERSION = ')(?\\d+)(?=';$)')
* ```
*
* ```js
* key: /(?<=^const ASSETS_VERSION = ')(?\d+)(?=';$)/
* ```
*/
/**
* @const {VersionOptions} defaultVersionOptions - The default shared version options.
* @const {VersionOptions} developmentVersionOptions - The predefined version options for development.
* @const {VersionOptions} productionVersionOptions - The predefined version options for production.
*/
export const defaultVersionOptions = {
prettyPrint: 4,
versionFormat: 'timestamp',
versionKey: 'version',
};
export const developmentVersionOptions = Object.assign({}, defaultVersionOptions);
export const productionVersionOptions = Object.assign({}, defaultVersionOptions);
/**
* @const {object} developmentVersionFilesArgs - The predefined `bumpVersion()` options for development.
* @const {object} productionVersionFilesArgs - The predefined `bumpVersion()` options for production.
*/
export const developmentVersionFilesArgs = [
developmentVersionOptions,
];
export const productionVersionFilesArgs = [
productionVersionOptions,
];
/**
* Bumps version numbers in file.
*
* @async
* @param {object} [versionOptions=null] - Customize the version options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function bumpVersions(versionOptions = null) {
if (versionOptions == null) {
versionOptions = productionVersionOptions;
} else if (
versionOptions !== developmentVersionOptions &&
versionOptions !== productionVersionOptions
) {
versionOptions = merge({}, defaultVersionOptions, versionOptions);
}
const queue = new Map();
/**
* @async
* @param {object} entry - The entrypoint to process.
* @param {string} entry.outfile - The file to write to.
* @param {?string} [entry.label] - The task label.
* Defaults to the outfile name.
* @param {?string} [entry.format] - The version number format.
* @param {?string} [entry.key] - The JSON field name assign the version number to.
* @param {?string|number} [entry.pretty] - The white space to use to format the JSON file.
* @return {Promise}
*/
loconfig.tasks.versions?.forEach(({
outfile,
label = null,
...options
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
options.pretty = (options.pretty ?? versionOptions.prettyPrint);
options.format = (options.format ?? versionOptions.versionFormat);
options.key = (options.key ?? versionOptions.versionKey);
if (queue.has(outfile)) {
queue.get(outfile).then(() => handleBumpVersion(outfile, label, options));
} else {
queue.set(outfile, handleBumpVersion(outfile, label, options));
}
});
};
/**
* Creates a formatted version number or string.
*
* @param {string} format - The version format.
* @param {?string} [oldValue] - The old version value.
* @return {string|number}
* @throws TypeError If the format or value are invalid.
*/
function createVersionNumber(format, oldValue = null) {
let [ type, modifier ] = format.split(':', 2);
switch (type) {
case 'hex':
case 'hexadecimal':
try {
modifier = Number.parseInt(modifier);
if (Number.isNaN(modifier)) {
modifier = 6;
}
return randomBytes(modifier).toString('hex');
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "hexadecimal"`,
{ cause: err }
);
}
case 'inc':
case 'increment':
try {
if (modifier === 'semver') {
return incrementSemVer(oldValue, [ 'buildmetadata', 'patch' ]);
}
return incrementNumber(oldValue, modifier);
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "increment"`,
{ cause: err }
);
}
case 'regex':
case 'regexp':
try {
return new RegExp(modifier);
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "regexp"`,
{ cause: err }
);
}
case 'timestamp':
return Date.now().valueOf();
}
throw new TypeError(
'Expected \'format\' to be either "timestamp", "increment", or "hexadecimal"'
);
}
/**
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @return {Promise}
*/
async function handleBumpVersion(outfile, label, options) {
const timeLabel = `${label} bumped in`;
console.time(timeLabel);
try {
options.key = parseVersionKey(options.key);
if (options.key instanceof RegExp) {
await handleBumpVersionWithRegExp(outfile, label, options);
} else {
await handleBumpVersionInJson(outfile, label, options);
}
message(`${label} bumped`, 'success', timeLabel);
} catch (err) {
message(`Error bumping ${label}`, 'error');
message(err);
notification({
title: `${label} bumping failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
}
/**
* Changes the version number for the provided JSON key in file.
*
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @param {string} options.key
* @return {Promise}
*/
async function handleBumpVersionInJson(outfile, label, options) {
outfile = resolve(outfile);
let json;
try {
json = JSON.parse(await readFile(outfile, { encoding: 'utf8' }));
} catch (err) {
json = {};
message(`${label} is a new file`, 'notice');
await mkdir(dirname(outfile), { recursive: true });
}
json[options.key] = createVersionNumber(options.format, json?.[options.key]);
return await writeFile(
outfile,
JSON.stringify(json, null, options.pretty),
{ encoding: 'utf8' }
);
}
/**
* Changes the version number for the provided RegExp in file.
*
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @param {RegExp} options.key
* @return {Promise}
*/
async function handleBumpVersionWithRegExp(outfile, label, options) {
outfile = resolve(outfile);
const bckfile = `${outfile}~`;
await rename(outfile, bckfile);
try {
const rl = readline.createInterface({
input: createReadStream(bckfile),
});
let newVersion = null;
const writeStream = createWriteStream(outfile, { encoding: 'utf8' });
rl.on('line', (line) => {
const found = line.match(options.key);
if (found) {
const groups = (found.groups ?? {});
const oldVersion = (groups.build ?? groups.version ?? found[1] ?? found[0]);
const newVersion = createVersionNumber(options.format, oldVersion);
const replacement = found[0].replace(oldVersion, newVersion);
line = line.replace(found[0], replacement);
}
writeStream.write(line + "\n");
});
await events.once(rl, 'close');
await rm(bckfile);
} catch (err) {
await rm(outfile, { force: true });
await rename(bckfile, outfile);
throw err;
}
}
/**
* Increments the given integer.
*
* @param {string|int} value - The number to increment.
* @param {string|int} [increment=1] - The amount to increment by.
* @return {int}
* @throws TypeError If the version number is invalid.
*/
function incrementNumber(value, increment = 1) {
const version = Number.parseInt(value);
if (Number.isNaN(version)) {
throw new TypeError(
`Expected an integer version number, received [${value}]`
);
}
increment = Number.parseInt(increment);
if (Number.isNaN(increment)) {
throw new TypeError(
'Expected an integer increment number'
);
}
return (version + increment);
}
/**
* Increments the given SemVer version number.
*
* @param {string} value - The version to mutate.
* @param {string|string[]} [target] - The segment to increment, one of:
* 'major', 'minor', 'patch', ~~'prerelease'~~, 'buildmetadata'.
* @param {string|int} [increment=1] - The amount to increment by.
* @return {string}
* @throws TypeError If the version or target are invalid.
*/
function incrementSemVer(value, target = 'patch', increment = 1) {
const found = value.match(REGEXP_SEMVER);
if (!found) {
throw new TypeError(
`Expected a SemVer version number, received [${value}]`
);
}
if (Array.isArray(target)) {
for (const group of target) {
if (found.groups[group] != null) {
target = group;
break;
}
}
}
if (!target || !found.groups[target]) {
throw new TypeError(
`Expected a supported SemVer segment, received [${target}]`
);
}
const segments = {
'major': '',
'minor': '.',
'patch': '.',
'prerelease': '-',
'buildmetadata': '+',
};
let replacement = '';
for (const [ segment, delimiter ] of Object.entries(segments)) {
if (found.groups?.[segment] != null) {
const newVersion = (segment === target)
? incrementNumber(found.groups[segment], increment)
: found.groups[segment];
replacement += `${delimiter}${newVersion}`;
}
}
return value.replace(found[0], replacement);
}
/**
* Parses the version key.
*
* @param {*} key - The version key.
* @return {string|RegExp}
*/
function parseVersionKey(key) {
if (key instanceof RegExp) {
return key;
}
if (typeof key !== 'string') {
throw new TypeError(
'Expected \'key\' to be either a string or a RegExp'
);
}
const delimiter = key.indexOf(':');
if (delimiter === -1) {
// Assumes its a JSON key
return key;
}
const type = key.slice(0, delimiter);
const value = key.slice(delimiter + 1);
switch (type) {
case 'json':
return value;
case 'regex':
case 'regexp':
return new RegExp(value);
}
throw new TypeError(
'Expected \'key\' type to be either "json" or "regexp"'
);
}
================================================
FILE: packages/landing/build/utils/index.js
================================================
/**
* @file Provides generic functions and constants.
*/
/**
* @type {RegExp} - Match all special characters.
*/
const regexUnescaped = /[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g;
/**
* Quotes regular expression characters.
*
* @param {string} str - The input string.
* @return {string} Returns the quoted (escaped) string.
*/
function escapeRegExp(str) {
return str.replace(regexUnescaped, '\\$&');
}
/**
* Creates a new object with all nested object properties
* concatenated into it recursively.
*
* Nested keys are flattened into a property path:
*
* ```js
* {
* a: {
* b: {
* c: 1
* }
* },
* d: 1
* }
* ```
*
* ```js
* {
* "a.b.c": 1,
* "d": 1
* }
* ```
*
* @param {object} input - The object to flatten.
* @param {string} prefix - The parent key prefix.
* @param {object} target - The object that will receive the flattened properties.
* @return {object} Returns the `target` object.
*/
function flatten(input, prefix, target = {}) {
for (const key in input) {
const field = (prefix ? prefix + '.' + key : key);
if (isObjectLike(input[key])) {
flatten(input[key], field, target);
} else {
target[field] = input[key];
}
}
return target;
}
/**
* Determines whether the passed value is an `Object`.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is an `Object`,
* otherwise `false`.
*/
function isObjectLike(value) {
return (value != null && typeof value === 'object');
}
/**
* Creates a new object with all nested object properties
* merged into it recursively.
*
* @param {object} target - The target object.
* @param {object[]} ...sources - The source object(s).
* @throws {TypeError} If the target and source are the same.
* @return {object} Returns the `target` object.
*/
function merge(target, ...sources) {
for (const source of sources) {
if (target === source) {
throw new TypeError(
'Cannot merge, target and source are the same'
);
}
for (const key in source) {
if (source[key] != null) {
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
merge(target[key], source[key]);
continue;
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
target[key] = target[key].concat(source[key]);
continue;
}
}
target[key] = source[key];
}
}
return target;
}
export {
escapeRegExp,
flatten,
isObjectLike,
merge,
regexUnescaped,
};
================================================
FILE: packages/landing/build/watch.js
================================================
import concatFiles, { developmentConcatFilesArgs } from './tasks/concats.js';
import compileScripts, { developmentScriptsArgs } from './tasks/scripts.js';
import compileStyles, { developmentStylesArgs } from './tasks/styles.js' ;
import compileSVGs, { developmentSVGsArgs } from './tasks/svgs.js';
import buildEleventy, { developmentEleventyOptions } from './tasks/eleventy.js';
import loconfig from './helpers/config.js';
import message from './helpers/message.js';
import notification from './helpers/notification.js';
import resolve from './helpers/template.js';
import { merge } from './utils/index.js';
import browserSync from 'browser-sync';
import { join } from 'node:path';
// Match a URL protocol.
const regexUrlStartsWithProtocol = /^[a-z0-9\-]:\/\//i;
// Build scripts, compile styles, concat files,
// and generate spritesheets on first hit
concatFiles(...developmentConcatFilesArgs);
compileScripts(...developmentScriptsArgs);
compileStyles(...developmentStylesArgs);
compileSVGs(...developmentSVGsArgs);
await buildEleventy(developmentEleventyOptions);
// Create a new BrowserSync instance
const server = browserSync.create();
// Start the BrowserSync server
server.init(createServerOptions(loconfig), (err) => {
if (err) {
message('Error starting development server', 'error');
message(err);
notification({
title: 'Development server failed',
message: `${err.name}: ${err.message}`
});
}
});
configureServer(server, loconfig);
/**
* Configures the BrowserSync options.
*
* @param {BrowserSync} server - The BrowserSync API.
* @param {object} loconfig - The project configset.
* @param {object} loconfig.paths - The paths options.
* @param {object} loconfig.tasks - The tasks options.
* @return {void}
*/
function configureServer(server, { paths, tasks }) {
const views = createViewsArray(paths.views);
// Reload on any changes to views or processed files
server.watch(
[
join(paths.dest, '**/*')
]
).on('change', server.reload);
// Watch source scripts
server.watch(
[
join(paths.scripts.src, '**/*.js'),
]
).on('change', () => {
compileScripts(...developmentScriptsArgs);
});
// Watch source concats
if (tasks.concats?.length) {
server.watch(
resolve(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
)
)
).on('change', () => {
concatFiles(...developmentConcatFilesArgs);
});
}
// Watch source styles
server.watch(
[
join(paths.styles.src, '**/*.scss'),
]
).on('change', () => {
compileStyles(...developmentStylesArgs);
});
// Watch source SVGs
server.watch(
[
join(paths.svgs.src, '*.svg'),
]
).on('change', () => {
compileSVGs(...developmentSVGsArgs);
});
}
/**
* Creates a new object with all the BrowserSync options.
*
* @param {object} loconfig - The project configset.
* @param {object} loconfig.paths - The paths options.
* @param {object} loconfig.server - The server options.
* @return {object} Returns the server options.
*/
function createServerOptions({
paths,
server: options
}) {
const config = {
open: false,
notify: false,
ghostMode: false
};
// Resolve the URL for the BrowserSync server
if (isNonEmptyString(paths.url)) {
// Use proxy
config.proxy = paths.url;
} else if (isNonEmptyString(paths.dest)) {
// Use base directory
config.server = {
baseDir: paths.dest
};
}
merge(config, resolve(options));
// If HTTPS is enabled, prepend `https://` to proxy URL
if (options?.https) {
if (isNonEmptyString(config.proxy?.target)) {
config.proxy.target = prependSchemeToUrl(config.proxy.target, 'https');
} else if (isNonEmptyString(config.proxy)) {
config.proxy = prependSchemeToUrl(config.proxy, 'https');
}
}
return config;
}
/**
* Creates a new array (shallow-copied) from the views configset.
*
* @param {*} views - The views configset.
* @throws {TypeError} If views is invalid.
* @return {array} Returns the views array.
*/
function createViewsArray(views) {
if (Array.isArray(views)) {
return Array.from(views);
}
switch (typeof views) {
case 'string':
return [ views ];
case 'object':
if (views != null) {
return Object.values(views);
}
}
throw new TypeError(
'Expected \'views\' to be a string, array, or object'
);
}
/**
* Prepends the scheme to the URL.
*
* @param {string} url - The URL to mutate.
* @param {string} [scheme] - The URL scheme to prepend.
* @return {string} Returns the mutated URL.
*/
function prependSchemeToUrl(url, scheme = 'http') {
if (regexUrlStartsWithProtocol.test(url)) {
return url.replace(regexUrlStartsWithProtocol, `${scheme}://`);
}
return `${scheme}://${url}`;
}
/**
* Determines whether the passed value is a string with at least one character.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is a non-empty string,
* otherwise `false`.
*/
function isNonEmptyString(value) {
return (typeof value === 'string' && value.length > 0);
}
================================================
FILE: packages/landing/data/features.json
================================================
{
"title": "Features",
"description": "Locomotive Scroll is a thin, opinionated wrapper around Lenis. You get all of Lenis's power plus our detection and animation layer.",
"scrollbar": {
"title": "Native scrollbar",
"description": "Real browser scrollbar. No fake alternatives. Accessible, performant, and familiar to users."
},
"normalized": {
"title": "Smooth easing",
"description": "Configurable lerp, duration, and custom easing functions thanks to Lenis options."
},
"sticky": {
"title": "CSS sticky",
"description": "Works perfectly with position: sticky. No conflicts, no workarounds."
},
"intersectionObserver": {
"title": "Intersection Observer API",
"description": "Browser-native detection. No polling, no performance hits, just efficient viewport tracking."
},
"scrollTo": {
"title": "Scroll to",
"description": "Programmatic scrolling to any element, selector, or pixel value via Lenis. Supports offset, duration, and custom easing."
},
"direction": {
"title": "Scroll direction",
"description": "Track direction changes in real-time thanks to Lenis. Perfect for hiding/showing headers or reversing animations."
},
"layoutShifts": {
"title": "No layout shifts",
"description": "No more greedy CSS transforms breaking your layouts. Plays nice with position: sticky, fixed headers, and existing CSS."
}
}
================================================
FILE: packages/landing/data/general.json
================================================
{
"title": "Locomotive Scroll",
"description": "A lightweight scroll library for modern web experiences. Detection, animation, and smooth scrolling — all in 9.4kB. Built on top of Lenis.",
"github": "https://github.com/locomotivemtl/locomotive-scroll",
"documentation": "https://scroll.locomotive.ca/docs",
"website": "https://locomotive.ca",
"version": "5.0"
}
================================================
FILE: packages/landing/data/metadata.json
================================================
{
"title": "Locomotive Scroll — Detection of elements in viewport & smooth scrolling with parallax effects",
"description": "Locomotive Scroll is a lightweight JavaScript library that provides smooth scrolling animations and advanced scroll interactions for web applications.",
"ogImage": "https://scroll.locomotive.ca/assets/images/og-image.png",
"ogImageWidth": "1200",
"ogImageHeight": "630",
"url": "https://scroll.locomotive.ca"
}
================================================
FILE: packages/landing/data/perks.json
================================================
{
"bigText": "Version 5 is a complete rewrite. Designed for modern workflows, built on top of Lenis, and optimized for production.",
"smallText": "This library has evolved considerably over the years. From jQuery to vanilla ES6, from custom engines to Lenis foundation.",
"items": [
{
"title": "Built on top of Lenis",
"description": "Latest stable release with improved touch handling and performance fixes. No more greedy CSS transforms!"
},
{
"title": "TypeScript First",
"description": "Fully typed. Better autocomplete, fewer bugs, happier developers."
},
{
"title": "Dual Intersection Observers",
"description": "Separate observers for simple triggers vs. continuous animations. No wasted RAF cycles."
},
{
"title": "Smart Touch Detection",
"description": "Parallax auto-disabled on mobile. Opt-in with one attribute."
},
{
"title": "Production Ready",
"description": "Accessible by default. Native scrollbar, keyboard nav, and proper cleanup for SPAs."
},
{
"title": "Lightweight",
"description": "Only 9.4kB gzipped"
}
]
}
================================================
FILE: packages/landing/data/showcase.json
================================================
{
"title": "Showcase",
"description": "Real projects built with Locomotive Scroll by leading studios and developers.",
"items": [
{
"title": "Locomotive",
"description": "https://locomotive.ca",
"url": "https://locomotive.ca"
},
{
"title": "Destigmatize",
"description": "https://2024.destigmatize.ca/",
"url": "https://2024.destigmatize.ca/"
},
{
"title": "Scout Motors",
"description": "https://scoutmotors.com/",
"url": "https://scoutmotors.com/"
},
{
"title": "Lightship",
"description": "https://lightshiprv.com/",
"url": "https://lightshiprv.com/"
},
{
"title": "Vooban",
"description": "https://vooban.com/",
"url": "https://vooban.com/"
},
{
"title": "Construction Desourdy",
"description": "https://constructiondesourdy.com/",
"url": "https://constructiondesourdy.com/"
},
{
"title": "GKC",
"description": "https://gkc.ca/",
"url": "https://gkc.ca/"
},
{
"title": "Troa",
"description": "https://www.troa.fr/",
"url": "https://www.troa.fr/"
},
{
"title": "Vazzi",
"description": "https://vazzi.fun/",
"url": "https://vazzi.fun/"
},
{
"title": "21TSI",
"description": "https://21tsi.com/",
"url": "https://21tsi.com/"
},
{
"title": "Eduard Bodak",
"description": "https://www.eduardbodak.com/",
"url": "https://www.eduardbodak.com/"
},
{
"title": "Mindmarket",
"description": "https://mindmarket.com/",
"url": "https://mindmarket.com/"
}
]
}
================================================
FILE: packages/landing/data/tools.json
================================================
{
"inview": {
"title": "In-view detection",
"description": "Add classes, trigger callbacks, or fire custom events when elements enter the viewport. Built on Intersection Observer for native performance."
},
"progress": {
"title": "Progress tracking",
"description": "Get real-time scroll progress (0-1) as CSS variables or JavaScript events. Perfect for progress bars, scroll-driven animations, or custom interactions."
},
"parallax": {
"title": "Parallax effects",
"description": "Create smooth parallax effects instantly with a single data-scroll-speed attribute. No complex setup, no math, just add a number and it works."
}
}
================================================
FILE: packages/landing/docs/development.md
================================================
# Development
* [Installation](#installation)
* [Usage](#usage)
* [Configuration](#configuration)
* [Environment Configuration](#environment-configuration)
* [Development Configuration](#development-configuration)
* [`paths` option](#paths-option)
* [`paths.url` option](#pathsurl-option)
* [`paths.dest` option](#pathsdest-option)
* [`tasks` option](#tasks-option)
* [`server` option](#server-option)
* [Tasks](#tasks)
* [`concats`](#concats)
* [`scripts`](#scripts)
* [`styles`](#styles)
* [`svgs`](#svgs)
* [`versions`](#versions)
---
The boilerplate provides a custom, easily configured, and very simple,
task runner for [Node] to process assets and test quickly in browsers.
Learn more about the boilerplate's [tasks](#tasks) below.
## Installation
Make sure you have the following installed:
* [Node] — at least 14.17, the latest LTS is recommended.
* [NPM] — at least 8.0, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
```sh
# Switch to recommended Node version from .nvmrc
nvm use
# Install dependencies from package.json
npm install
```
## Usage
```sh
# Start development server, watch for changes, and compile assets
npm start
# Compile and minify assets
npm run build
```
See [`build.js`](../build/build.js) and [`watch.js`](../build/watch.js)
for details.
## Configuration
For development, most configuration values for processing front-end assets
are defined in the [`loconfig.json`](../loconfig.json) file that exists at
the root directory of your project.
### Environment Configuration
If any configuration options vary depending on whether your project is
running on your computer, a collaborator's computer, or on a web server,
these values should be stored in a `loconfig.local.json` file.
In fresh copy of the boilerplate, the root directory of your project
will contain a [`loconfig.example.json`](../loconfig.example.json) file.
> 💡 The boilerplate's default example customizes the development server
> to use a custom SSL certificate.
That file can be copied to `loconfig.local.json` and customized to suit
your local environment.
Your `loconfig.local.json` _should not_ be committed to your project's
source control.
> 💡 If you are developing with a team, you may wish to continue
> including a `loconfig.example.json` file with your project.
### Development Configuration
The boilerplate provides a few configuration settings to control the
behaviour for processing front-end assets.
#### `paths` option
The `paths` option defines URIs and file paths.
It is primarily used for template tags to reference any configuration
properties to reduce repetition.
Template tags are specified using `{% %}` delimiters. They will be
automatically expanded when tasks process paths.
```jsonc
{
"paths": {
"styles": {
"src": "./assets/styles",
"dest": "./www/assets/styles"
}
},
"tasks": {
"styles": [
{
"infile": "{% paths.styles.src %}/main.scss", // → ./assets/styles/main.scss
"outfile": "{% paths.styles.dest %}/main.css" // → ./www/assets/styles/main.css
}
]
}
}
```
#### `paths.url` option
The `paths.url` option defines the base URI of the project.
By default, it is used by the development server as a proxy
for an existing virtual host.
```json
{
"paths": {
"url": "locomotive-boilerplate.test"
}
}
```
#### `paths.dest` option
The `paths.dest` option defines the public web directory of the project.
By default, it is used by the development server as the base directory
to serve the website from if a proxy URI is not provided.
```json
{
"paths": {
"dest": "./www"
}
}
```
#### `tasks` option
Which assets and how they should be processed can be configured via
the `tasks` option:
```json
{
"tasks": {
"scripts": [
{
"includes": [
"./assets/scripts/app.js"
],
"outfile": "./www/assets/scripts/app.js"
}
],
"styles": [
{
"infile": "./assets/styles/main.scss",
"outfile": "./www/assets/styles/main.css"
}
]
}
}
```
See [tasks](#tasks) section, below, for details.
#### `server` option
The development server (BrowserSync) can be configured via
the `server` option:
```json
{
"server": {
"open": true,
"https": {
"key": "~/.config/valet/Certificates/{% paths.url %}.key",
"cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
}
}
}
```
Visit [BrowserSync's documentation](https://browsersync.io/docs/options)
for all options.
## Tasks
The boilerplate provides a handful of tasks for handling
the most commonly processed assets.
### `concats`
A wrapper around [concat] (with optional support for globbing) for concatenating multiple files.
By default, [tiny-glob] is installed with the boilerplate.
Example:
```json
{
"concats": [
{
"label": "Application Vendors",
"includes": [
"{% paths.scripts.src %}/vendors/*.js",
"node_modules/focus-visible/dist/focus-visible.min.js",
"node_modules/vue/dist/vue.min.js",
"node_modules/vuelidate/dist/vuelidate.min.js",
"node_modules/vuelidate/dist/validators.min.js"
],
"outfile": "{% paths.scripts.dest %}/app/vendors.js"
},
{
"label": "Public Site Vendors",
"includes": [
"{% paths.scripts.src %}/vendors/*.js",
"node_modules/focus-visible/dist/focus-visible.min.js"
],
"outfile": "{% paths.scripts.dest %}/site/vendors.js"
}
]
}
```
See [`concats.js`](../build/tasks/concats.js) for details.
### `scripts`
A wrapper around [esbuild] for bundling and minifying modern JS/ES modules.
Example:
```json
{
"scripts": [
{
"label": "Application Dashboard JS",
"includes": [
"{% paths.scripts.src %}/app/dashboard.js"
],
"outfile": "{% paths.scripts.dest %}/app/dashboard.js"
},
{
"label": "Public Site JS",
"includes": [
"{% paths.scripts.src %}/site/main.js"
],
"outfile": "{% paths.scripts.dest %}/site/main.js"
}
]
}
```
See [`scripts.js`](../build/tasks/scripts.js) for details.
### `styles`
A wrapper around [sass] (with optional support for [Autoprefixer]
via [PostCSS]) for compiling and minifying Sass into CSS.
By default, [PostCSS] and [Autoprefixer] are installed with the boilerplate.
Example:
```json
{
"styles": [
{
"label": "Text Editor CSS",
"infile": "{% paths.styles.src %}/app/editor.scss",
"outfile": "{% paths.styles.dest %}/app/editor.css"
},
{
"label": "Application Dashboard CSS",
"infile": "{% paths.styles.src %}/app/dashboard.scss",
"outfile": "{% paths.styles.dest %}/app/dashboard.css"
},
{
"label": "Public Site Critical CSS",
"infile": "{% paths.styles.src %}/site/critical.scss",
"outfile": "{% paths.styles.dest %}/site/critical.css"
},
{
"label": "Public Site CSS",
"infile": "{% paths.styles.src %}/site/main.scss",
"outfile": "{% paths.styles.dest %}/site/main.css"
}
]
}
```
See [`styles.js`](../build/tasks/styles.js) for details.
The task also supports [PurgeCSS] to remove unused CSS.
See the [documentation on our Grid System](grid.md#build-tasks) for details.
### `svgs`
A wrapper around [SVG Mixer] for transforming and minifying SVG files
and generating spritesheets.
Example:
```json
{
"svgs": [
{
"label": "Application Spritesheet",
"includes": [
"{% paths.images.src %}/app/*.svg"
],
"outfile": "{% paths.svgs.dest %}/app/sprite.svg"
},
{
"label": "Public Site Spritesheet",
"includes": [
"{% paths.images.src %}/site/*.svg"
],
"outfile": "{% paths.svgs.dest %}/site/sprite.svg"
}
]
}
```
See [`svgs.js`](../build/tasks/svgs.js) for details.
### `versions`
A task to create and update values for use in versioning assets.
Can generate a hexadecimal value (using random bytes), use the current timestamp,
or increment a number.
Example:
```json
{
"versions": [
{
"format": "timestamp",
"key": "now",
"outfile": "./assets.json"
},
{
"format": "hex:8",
"key": "hex",
"outfile": "./assets.json"
},
{
"format": "inc:semver",
"key": "inc",
"outfile": "./assets.json"
}
]
}
```
```json
{
"now": 1665071717350,
"hex": "6ef54181c4ba",
"hex": "1.0.2"
}
```
The task supports replacing the value of a data key in a JSON file or replacing
a string in a file using a [regular expression](RegExp).
* Explicit JSON field name:
```json
{
"key": "json:version"
}
```
* Implicit JSON field name:
```json
{
"key": "version"
}
```
The regular expression can be a `RegExp` object or a pattern prefixed with `regexp:`.
* ```json
{
"key": "regexp:(?<=^const ASSETS_VERSION = ')(?\\d+)(?=';$)"
}
```
* ```js
{
key: new RegExp('(?<=^const ASSETS_VERSION = ')(?\\d+)(?=';$)')
}
```
* ```js
{
key: /^ \* Version: +(?:.+?)\+(.+?)$/
}
```
The regular expression pattern will match the first occurrence and replace
the first match in the following order: `build` (named capture), `version`
(named capture), `1` (first capture), or `0` (whole match).
See [`versions.js`](../build/tasks/versions.js) for details.
[Autoprefixer]: https://npmjs.com/package/autoprefixer
[BrowserSync]: https://npmjs.com/package/browser-sync
[concat]: https://npmjs.com/package/concat
[esbuild]: https://npmjs.com/package/esbuild
[fast-glob]: https://npmjs.com/package/fast-glob
[glob]: https://npmjs.com/package/glob
[globby]: https://npmjs.com/package/globby
[Node]: https://nodejs.org/
[sass]: https://npmjs.com/package/sass
[NPM]: https://npmjs.com/
[NVM]: https://github.com/nvm-sh/nvm
[PostCSS]: https://npmjs.com/package/postcss
[PurgeCSS]: https://purgecss.com/
[RegExp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[SVG Mixer]: https://npmjs.com/package/svg-mixer
[tiny-glob]: https://npmjs.com/package/tiny-glob
================================================
FILE: packages/landing/docs/grid.md
================================================
# Grid system
* [Architectures](#architecture)
* [Build tasks](#build-tasks)
* [Configuration](#configuration)
* [Usage](#usage)
* [Example](#example)
## Architecture
The boilerplate's grid system is meant to be simple and easy to use. The goal is to create a light, flexible, and reusable way to build layouts.
The following styles are needed to work properly:
* [`o-grid`](../assets/styles/objects/_grid.scss) — Object file where the default grid styles are set such as column numbers, modifiers, and options.
* [`u-grid-columns`](../assets/styles/utilities/_grid-column.scss) — Utility file that generates the styles for every possible column based on an array of media queries and column numbers.
### Build tasks
The columns generated by [`u-grid-columns`](../assets/styles/utilities/_grid-column.scss) adds a lot of styles to the compiled CSS file. To mitigate that, [PurgeCSS] is integrated into the `styles` build task to purge unused CSS.
#### Configuration
Depending on your project, you will need to specify all the files that include CSS classes from the grid system. These files will be scanned by [PurgeCSS] to your compiled CSS files.
Example of a Charcoal project:
```jsonc
"purgeCSS": {
"content": [
"./views/app/template/**/*.mustache",
"./src/App/Template/*.php",
"./assets/scripts/**/*" // use case: `el.classList.add('u-gc-1/2')`
]
}
```
## Usage
The first step is to set intial SCSS values in the following files :
- [`settings/_config.scss`](../assets/styles/settings/_config.scss)
```scss
// Grid
// ==========================================================================
$base-column-nb: 12;
$base-column-gap: $unit-small;
```
You can create multiple column layouts depending on media queries.
- [`objects/_grid.scss`](../assets/styles/objects/_grid.scss)
```scss
.o-grid {
display: grid;
width: 100%;
margin: 0;
padding: 0;
list-style: none;
// ==========================================================================
// Cols
// ==========================================================================
&.-col-#{$base-column-nb} {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
&.-col-4 {
grid-template-columns: repeat(4, 1fr);
}
&.-col-#{$base-column-nb}\@from-medium {
@media (min-width: $from-medium) {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
}
// …
```
### Example
The following layout has 4 columns at `>=999px` and 12 columns at `<1000px`.
```html
Hello
This grid has 4 columns and 12 columns from `medium` MQ
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?
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?
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?
```
[PurgeCSS]: https://purgecss.com/
================================================
FILE: packages/landing/docs/technologies.md
================================================
# Technologies
* [Styles](#styles)
* [CSS Architecture](#css-architecture)
* [CSS Naming Convention](#css-naming-convention)
* [CSS Namespacing](#css-namespacing)
* [Example](#example-1)
* [Scripts](#scripts)
* [Example](#example-2)
* [Page transitions](#page-transitions)
* [Example](#example-3)
* [Scroll detection](#scroll-detection)
* [Example](#example-4)
## Styles
[SCSS][Sass] is a superset of CSS that adds many helpful features to improve
and modularize our styles.
We use [node-sass] (LibSass) for processing and minifying SCSS into CSS.
We also use [PostCSS] and [Autoprefixer] to parse our CSS and add
vendor prefixes for experimental features.
### CSS Architecture
The boilerplate's CSS architecture is based on [Inuit CSS][inuitcss] and [ITCSS].
* `settings`: Global variables, site-wide settings, config switches, etc.
* `tools`: Site-wide mixins and functions.
* `generic`: Low-specificity, far-reaching rulesets (e.g. resets).
* `elements`: Unclassed HTML elements (e.g. `a {}`, `blockquote {}`, `address {}`).
* `objects`: Objects, abstractions, and design patterns (e.g. `.o-layout {}`).
* `components`: Discrete, complete chunks of UI (e.g. `.c-carousel {}`).
* `utilities`: High-specificity, very explicit selectors. Overrides and helper
classes (e.g. `.u-hidden {}`)
Learn more about [Inuit CSS](https://github.com/inuitcss/inuitcss#css-directory-structure).
### CSS Naming Convention
We use a simplified [BEM] (Block, Element, Modifier) syntax:
* `.block`
* `.block_element`
* `.-modifier`
### CSS Namespacing
We namespace our classes for more UI transparency:
* `o-`: Object that it may be used in any number of unrelated contexts to the one you can currently see it in. Making modifications to these types of class could potentially have knock-on effects in a lot of other unrelated places.
* `c-`: Component is a concrete, implementation-specific piece of UI. All of the changes you make to its styles should be detectable in the context you’re currently looking at. Modifying these styles should be safe and have no side effects.
* `u-`: Utility has a very specific role (often providing only one declaration) and should not be bound onto or changed. It can be reused and is not tied to any specific piece of UI.
* `s-`: Scope creates a new styling context. Similar to a Theme, but not necessarily cosmetic, these should be used sparingly—they can be open to abuse and lead to poor CSS if not used wisely.
* `is-`, `has-`: Is currently styled a certain way because of a state or condition. It tells us that the DOM currently has a temporary, optional, or short-lived style applied to it due to a certain state being invoked.
Learn about [namespacing](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/).
### Example \#1
```html
```
```scss
.c-block {
&.-large {
padding: rem(60px);
}
}
.c-block_heading {
@media (max-width: $to-medium) {
.c-block.-large & {
margin-bottom: rem(40px);
}
}
}
```
## Scripts
We use [esbuild] for bundling and minifying JavaScript/ES modules.
[modularJS] is a small framework we use on top of ES modules.
* Automatically init visible modules.
* Easily call other modules methods.
* Quickly set scoped events with delegation.
* Simply select DOM elements scoped in their module.
[_source_](https://npmjs.com/package/modujs#why)
### Example \#2
```html
Example
```
```js
import { module } from 'modujs';
export default class extends module {
constructor(m) {
super(m);
this.events = {
click: {
load: 'loadMore'
}
};
}
loadMore() {
this.$('main')[0].classList.add('is-loading');
}
}
```
Learn more about [modularJS].
## Page transitions
[modularLoad] is used for page transitions and lazy loading.
### Example \#3
```html
```
```js
import modularLoad from 'modularload';
this.load = new modularLoad({
enterDelay: 300,
transitions: {
transitionName: {
enterDelay: 450
}
}
});
```
Learn more about [modularLoad].
## Scroll detection
[Locomotive Scroll][locomotive-scroll] is used for elements in viewport
detection and smooth scrolling with parallax.
### Example \#4
```html
Separate observers for simple triggers vs. continuous animations. No wasted RAF cycles.
Smart Touch Detection
Parallax auto-disabled on mobile. Opt-in with one attribute.
Production Ready
Accessible by default. Native scrollbar, keyboard nav, and proper cleanup for SPAs.
Lightweight
Only 9.4kB gzipped
Version 5 is a complete rewrite. Designed for modern workflows, built on top of Lenis, and optimized for production.
⛵
This library has evolved considerably over the years. From jQuery to vanilla ES6, from custom engines to Lenis foundation.
Locomotive®Scroll
Locomotive®Scroll
Locomotive®Scroll
Locomotive®Scroll
Locomotive®Scroll
Locomotive®Scroll
Locomotive®Scroll
Locomotive®Scroll
👀
Built-in
Tools
🔴Work faster
Work smarter🔴
01In-view detectionAdd classes, trigger callbacks, or fire custom events when elements enter the viewport. Built on Intersection Observer for native performance.
In-view detection
02Progress trackingGet real-time scroll progress (0-1) as CSS variables or JavaScript events. Perfect for progress bars, scroll-driven animations, or custom interactions.
Progress tracking
02Parallax effectsCreate smooth parallax effects instantly with a single data-scroll-speed attribute. No complex setup, no math, just add a number and it works.
Parallax effects
Features
⛵
Locomotive Scroll is a thin, opinionated wrapper around Lenis. You get all of Lenis's power plus our detection and animation layer.