├── .gitignore ├── .npmrc ├── README.md ├── app.vue ├── assets └── fonts │ ├── PPMori-Regular.woff │ └── PPMori-Regular.woff2 ├── components └── hello.vue ├── constants └── media.ts ├── interactions ├── classes │ ├── dom.ts │ ├── observer-animation.ts │ └── scroll-animation.ts ├── components │ └── hello.ts ├── index.ts ├── types.ts └── utils.ts ├── nuxt.config.ts ├── package.json ├── public └── favicon.ico ├── styles ├── _abstracts │ ├── _functions.scss │ ├── _mixins.scss │ └── _variables.scss ├── _base │ ├── _fonts.scss │ ├── _globals.scss │ └── _reset.scss ├── _components │ └── _hello.scss ├── _vendor │ ├── _include-media.scss │ └── _lenis.scss └── index.scss ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt - Gsap Starter Template 2 | 3 | - NUXT Version 3 💚 4 | - GSAP Classes Setup 🧩 5 | - LENIS native smooth scroll 🏄 6 | - SASS 7-1 Architecture 🛠️ 7 | 8 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 9 | 10 | ## Setup 11 | 12 | Make sure to install the dependencies: 13 | 14 | ```bash 15 | # npm 16 | npm install 17 | 18 | # pnpm 19 | pnpm install 20 | 21 | # yarn 22 | yarn install 23 | ``` 24 | 25 | ## Development Server 26 | 27 | Start the development server on `http://localhost:3000`: 28 | 29 | ```bash 30 | # npm 31 | npm run dev 32 | 33 | # pnpm 34 | pnpm run dev 35 | 36 | # yarn 37 | yarn dev 38 | ``` 39 | 40 | ## Production 41 | 42 | Build the application for production: 43 | 44 | ```bash 45 | # npm 46 | npm run build 47 | 48 | # pnpm 49 | pnpm run build 50 | 51 | # yarn 52 | yarn build 53 | ``` 54 | 55 | Locally preview production build: 56 | 57 | ```bash 58 | # npm 59 | npm run preview 60 | 61 | # pnpm 62 | pnpm run preview 63 | 64 | # yarn 65 | yarn preview 66 | ``` 67 | 68 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 69 | 70 | ## Deploy On Brimble 71 | 72 | 73 | Brimble badge 74 | 75 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /assets/fonts/PPMori-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kadetXx/nuxt-gsap-starter/c0ec1dd6f8b342eb146f92c29cc4b37c21f331b9/assets/fonts/PPMori-Regular.woff -------------------------------------------------------------------------------- /assets/fonts/PPMori-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kadetXx/nuxt-gsap-starter/c0ec1dd6f8b342eb146f92c29cc4b37c21f331b9/assets/fonts/PPMori-Regular.woff2 -------------------------------------------------------------------------------- /components/hello.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /constants/media.ts: -------------------------------------------------------------------------------- 1 | export const media = {}; 2 | -------------------------------------------------------------------------------- /interactions/classes/dom.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import { getElements } from "../utils"; 3 | 4 | export class Dom extends EventEmitter { 5 | element: HTMLElement; 6 | secondaryElements: SecondaryElements; 7 | 8 | constructor({ selector, secondarySelectors }: SelectorProps) { 9 | super(); 10 | 11 | const { element, secondaryElements } = getElements({ 12 | selector, 13 | secondarySelectors, 14 | }); 15 | 16 | this.element = element; 17 | this.secondaryElements = secondaryElements; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /interactions/classes/observer-animation.ts: -------------------------------------------------------------------------------- 1 | import { Dom } from "./dom"; 2 | import gsap from "gsap"; 3 | 4 | export class ObserverAnimation extends Dom { 5 | timeline: gsap.core.Timeline; 6 | observer: IntersectionObserver; 7 | target: ObserverAnimationProps["target"]; 8 | animation: ObserverAnimationProps["animation"]; 9 | 10 | constructor({ 11 | selector, 12 | target, 13 | animation, 14 | initialState, 15 | }: ObserverAnimationProps) { 16 | super({ 17 | selector, 18 | secondarySelectors: {}, 19 | }); 20 | 21 | initialState && this.initialize(initialState); 22 | 23 | this.timeline = gsap.timeline(); 24 | this.animation = animation; 25 | this.target = target; 26 | 27 | this.createObserver(); 28 | } 29 | 30 | createObserver() { 31 | const callback = (entries: IntersectionObserverEntry[]) => { 32 | entries.forEach(entry => { 33 | entry.isIntersecting ? this.animateIn() : this.animateOut(); 34 | }); 35 | }; 36 | 37 | this.observer = new window.IntersectionObserver(callback, { 38 | threshold: this.animation?.threshold ?? 0.5, 39 | rootMargin: this.animation?.rootMargin, 40 | root: this.animation?.root, 41 | }); 42 | 43 | this.observer.observe(this.target ?? this.element); 44 | } 45 | 46 | initialize(tween: gsap.TweenVars) { 47 | gsap.set(this.element, { 48 | ...tween, 49 | }); 50 | } 51 | 52 | animateIn() { 53 | return new Promise(resolve => { 54 | this.timeline.to(this.element, { 55 | ...this.animation?.tween, 56 | }); 57 | 58 | this.timeline.call(resolve); 59 | }); 60 | } 61 | 62 | animateOut() { 63 | return new Promise(resolve => { 64 | if (this.animation?.resetOnExit) { 65 | this.timeline.reverse(); 66 | } 67 | 68 | this.timeline.call(resolve); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /interactions/classes/scroll-animation.ts: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import { Dom } from "./dom"; 3 | 4 | export class ScrollAnimation extends Dom { 5 | timeline: GSAPTimeline; 6 | 7 | constructor({ element, animationProps }: ScrollAnimationProps) { 8 | super({ selector: element, secondarySelectors: {} }); 9 | this.animate(animationProps); 10 | } 11 | 12 | animate(props: ScrollAnimationProps["animationProps"]) { 13 | const { scrub, trigger, start, ...rest } = props; 14 | 15 | gsap.set(this.element, { 16 | ...props.initialState, 17 | }); 18 | 19 | this.timeline = gsap.timeline({ 20 | scrollTrigger: { 21 | ...rest, 22 | start: start ?? "top bottom", 23 | scrub: scrub ?? 0.7, 24 | trigger: trigger ?? this.element, 25 | }, 26 | }); 27 | 28 | props.function && props.function(this.timeline); 29 | } 30 | 31 | async kill() { 32 | await this.timeline?.kill(); 33 | return Promise.resolve(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /interactions/components/hello.ts: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import { Dom } from "../classes/dom"; 3 | import { ScrollAnimation } from "../classes/scroll-animation"; 4 | 5 | export class Hello extends Dom { 6 | timeline: GSAPTimeline; 7 | animations: ScrollAnimation[] = []; 8 | 9 | constructor() { 10 | super({ 11 | selector: ".hello", 12 | secondarySelectors: { 13 | dash: ".hello__dash", 14 | }, 15 | }); 16 | 17 | this.timeline = gsap.timeline(); 18 | this.init(); 19 | } 20 | 21 | init() { 22 | this.animations.push( 23 | new ScrollAnimation({ 24 | element: this.secondaryElements.dash[0], 25 | animationProps: { 26 | pin: true, 27 | trigger: this.element, 28 | start: "top top", 29 | 30 | function: tl => { 31 | tl.to(this.secondaryElements.dash[0], { 32 | width: "40vw", 33 | }); 34 | }, 35 | }, 36 | }) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /interactions/index.ts: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import Lenis from "@studio-freight/lenis"; 3 | import ScrollTrigger from "gsap/ScrollTrigger"; 4 | import { Dom } from "./classes/dom"; 5 | import { Hello } from "./components/hello"; 6 | 7 | export class Interactions { 8 | components: { [x: string]: Dom } = {}; 9 | 10 | constructor() { 11 | gsap.registerPlugin(ScrollTrigger); 12 | 13 | this.onResize(); 14 | this.createSmoothScroll(); 15 | this.createPreloader(); 16 | this.addEventListeners(); 17 | } 18 | 19 | createSmoothScroll() { 20 | const lenis = new Lenis({ 21 | lerp: 0.1, 22 | }); 23 | 24 | lenis.on("scroll", ScrollTrigger.update); 25 | 26 | gsap.ticker.add(time => { 27 | lenis.raf(time * 1000); 28 | }); 29 | 30 | gsap.ticker.lagSmoothing(0); 31 | } 32 | 33 | createPreloader() { 34 | /** 35 | * - preload media 36 | * - initializations 37 | * - call onpreloaded on conpletion 38 | * */ 39 | this.onPreloaded(); 40 | } 41 | 42 | onPreloaded() { 43 | this.components.hello = new Hello(); 44 | } 45 | 46 | onResize() { 47 | /** 48 | * - recalculate 49 | * - reinitialise 50 | * - reset, etc. 51 | * */ 52 | } 53 | 54 | addEventListeners() { 55 | window.addEventListener("resize", this.onResize.bind(this)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /interactions/types.ts: -------------------------------------------------------------------------------- 1 | interface SelectorProps { 2 | selector: string | HTMLElement; 3 | secondarySelectors: { 4 | [x: string]: string | HTMLElement; 5 | }; 6 | } 7 | 8 | interface ScrollAnimationSettings { 9 | initialState?: gsap.TweenVars; 10 | function?: (tl: GSAPTimeline) => void; 11 | } 12 | 13 | interface ScrollAnimationProps { 14 | element: string | HTMLElement; 15 | animationProps: ScrollTrigger.Vars & ScrollAnimationSettings; 16 | } 17 | 18 | type SecondaryElements = { 19 | [x: string]: HTMLElement[]; 20 | }; 21 | 22 | interface ObserverAnimationProps 23 | extends Omit { 24 | initialState?: gsap.TweenVars; 25 | target?: HTMLElement | null; 26 | animation?: IntersectionObserverInit & { 27 | tween?: gsap.TweenVars; 28 | resetOnExit?: boolean; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /interactions/utils.ts: -------------------------------------------------------------------------------- 1 | export const getElements = ({ selector, secondarySelectors }: SelectorProps) => { 2 | const isEl = selector instanceof window.HTMLElement; 3 | const dummyDiv = document.createElement("div"); 4 | 5 | const element: HTMLElement = isEl 6 | ? selector 7 | : document.querySelector(selector) ?? dummyDiv; 8 | 9 | const secondaryElements: SecondaryElements = {}; 10 | 11 | Object.entries(secondarySelectors).forEach(([key, item]) => { 12 | const isEl = item instanceof window.HTMLElement; 13 | secondaryElements[key] = isEl 14 | ? [item] 15 | : Array.from(document.querySelectorAll(item)); 16 | }); 17 | 18 | return { element, secondaryElements }; 19 | }; 20 | 21 | export const getDistanceFromMidViewport = (element: HTMLElement) => { 22 | const vw = window.innerWidth; 23 | const vh = window.innerHeight; 24 | 25 | const { x, y, width, height }: DOMRect = element.getBoundingClientRect(); 26 | 27 | const yCenter = y + height / 2; 28 | const xCenter = x + width / 2; 29 | 30 | const distY = vh / 2 - yCenter; 31 | const distX = vw / 2 - xCenter; 32 | 33 | return { x: distX, y: distY }; 34 | }; 35 | 36 | export const preloadMedia = (media: HTMLImageElement, callback: () => void) => { 37 | const src = media.getAttribute("data-src"); 38 | 39 | if (!src) return; 40 | 41 | const fakeImage: HTMLImageElement = new Image(); 42 | fakeImage.src = src; 43 | 44 | fakeImage.onload = () => { 45 | media.src = src; 46 | callback(); 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: false }, 4 | css: ["@/styles/index.scss"], 5 | }); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "scripts": { 5 | "build": "nuxt build", 6 | "dev": "nuxt dev", 7 | "generate": "nuxt generate", 8 | "preview": "nuxt preview", 9 | "postinstall": "nuxt prepare" 10 | }, 11 | "devDependencies": { 12 | "@nuxt/devtools": "latest", 13 | "nuxt": "^3.7.0", 14 | "sass": "^1.66.1" 15 | }, 16 | "dependencies": { 17 | "@studio-freight/lenis": "^1.0.19", 18 | "events": "^3.3.0", 19 | "gsap": "^3.12.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kadetXx/nuxt-gsap-starter/c0ec1dd6f8b342eb146f92c29cc4b37c21f331b9/public/favicon.ico -------------------------------------------------------------------------------- /styles/_abstracts/_functions.scss: -------------------------------------------------------------------------------- 1 | /* z-index */ 2 | 3 | @function z($name) { 4 | @if index($z-indexes, $name) { 5 | @return (length($z-indexes) - index($z-indexes, $name)) + 1; 6 | } @else { 7 | @warn 'There is no item "#{$name}" in this list; Choose one of: #{$z-indexes}'; 8 | 9 | @return null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /styles/_abstracts/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* flexbox */ 2 | @mixin flex($justify: flex-start, $align: flex-start, $gap: 0) { 3 | display: flex; 4 | justify-content: $justify; 5 | align-items: $align; 6 | gap: $gap; 7 | } 8 | 9 | /* grid */ 10 | @mixin grid($columns: 1fr, $gap: 0, $items: center) { 11 | display: grid; 12 | place-items: $items; 13 | grid-template-columns: $columns; 14 | } 15 | -------------------------------------------------------------------------------- /styles/_abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $col-black: #0c0c0e; 3 | $col-white: #ffffff; 4 | $col-grey: #6b6b6b; 5 | 6 | // fonts 7 | $font-mori: "PP Mori", Arial, Helvetica, sans-serif; 8 | 9 | // z-indexes 10 | $z-indexes: ( 11 | "level-six", 12 | "level-five", 13 | "level-four", 14 | "level-three", 15 | "level-two", 16 | "level-one" 17 | ); 18 | -------------------------------------------------------------------------------- /styles/_base/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "PP Mori"; 3 | src: url("~/assets/fonts/PPMori-Regular.woff2") format("woff2"), 4 | url("~/assets/fonts/PPMori-Regular.woff") format("woff"); 5 | font-weight: normal; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | -------------------------------------------------------------------------------- /styles/_base/_globals.scss: -------------------------------------------------------------------------------- 1 | * { 2 | outline: none; 3 | box-sizing: border-box; 4 | scroll-behavior: smooth; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | text-decoration: none; 8 | text-rendering: geometricPrecision; 9 | } 10 | 11 | html { 12 | padding: 0; 13 | margin: 0; 14 | overscroll-behavior: none; 15 | } 16 | 17 | body { 18 | padding: 0; 19 | margin: 0; 20 | font-size: 1.6rem; 21 | line-height: 100%; 22 | scroll-behavior: smooth; 23 | font-family: $font-mori; 24 | background-color: $col-black; 25 | } 26 | 27 | figure { 28 | flex-shrink: 0; 29 | display: grid; 30 | place-items: center; 31 | } 32 | 33 | figure img { 34 | max-width: 100%; 35 | max-height: 100%; 36 | } 37 | 38 | a, 39 | a:hover { 40 | color: inherit; 41 | } 42 | 43 | main { 44 | position: relative; 45 | min-height: 100vh; 46 | will-change: transform; 47 | } 48 | -------------------------------------------------------------------------------- /styles/_base/_reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font: inherit; 87 | vertical-align: baseline; 88 | } 89 | 90 | article, 91 | aside, 92 | details, 93 | figcaption, 94 | figure, 95 | footer, 96 | header, 97 | hgroup, 98 | menu, 99 | nav, 100 | section { 101 | display: block; 102 | } 103 | 104 | ol, 105 | ul { 106 | list-style: none; 107 | } 108 | 109 | blockquote, 110 | q { 111 | quotes: none; 112 | } 113 | 114 | blockquote:before, 115 | blockquote:after, 116 | q:before, 117 | q:after { 118 | content: ""; 119 | content: none; 120 | } 121 | 122 | table { 123 | border-collapse: collapse; 124 | border-spacing: 0; 125 | } 126 | 127 | input::-webkit-outer-spin-button, 128 | input::-webkit-inner-spin-button { 129 | -webkit-appearance: none; 130 | margin: 0; 131 | } 132 | 133 | input[type="number"] { 134 | -moz-appearance: textfield; 135 | } 136 | -------------------------------------------------------------------------------- /styles/_components/_hello.scss: -------------------------------------------------------------------------------- 1 | .hello { 2 | height: 100vh; 3 | @include flex(center, center, 1rem); 4 | } 5 | 6 | .hello__text { 7 | font-size: 6rem; 8 | font-weight: 900; 9 | -webkit-text-stroke-width: 6px; 10 | -webkit-text-stroke-color: $col-grey; 11 | font-family: $font-mori; 12 | color: $col-grey; 13 | } 14 | 15 | .hello__dash { 16 | height: 1.4rem; 17 | width: 4rem; 18 | margin-top: 1rem; 19 | background-color: $col-grey; 20 | } 21 | -------------------------------------------------------------------------------- /styles/_vendor/_include-media.scss: -------------------------------------------------------------------------------- 1 | // _ _ _ _ _ 2 | // (_) | | | | | (_) 3 | // _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _ 4 | // | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` | 5 | // | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| | 6 | // |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_| 7 | // 8 | // Simple, elegant and maintainable media queries in Sass 9 | // v1.4.9 10 | // 11 | // https://eduardoboucas.github.io/include-media 12 | // 13 | // Authors: Eduardo Boucas (@eduardoboucas) 14 | // Kitty Giraudel (@kittygiraudel) 15 | // 16 | // This project is licensed under the terms of the MIT license 17 | //// 18 | /// include-media library public configuration 19 | /// @author Eduardo Boucas 20 | /// @access public 21 | //// 22 | 23 | /// 24 | /// Creates a list of global breakpoints 25 | /// 26 | /// @example scss - Creates a single breakpoint with the label `phone` 27 | /// $breakpoints: ('phone': 320px); 28 | /// 29 | $breakpoints: ( 30 | "mobile": 700px, 31 | "tablet": 950px, 32 | "desktop": 1440px, 33 | ) !default; 34 | 35 | /// 36 | /// Creates a list of static expressions or media types 37 | /// 38 | /// @example scss - Creates a single media type (screen) 39 | /// $media-expressions: ('screen': 'screen'); 40 | /// 41 | /// @example scss - Creates a static expression with logical disjunction (OR operator) 42 | /// $media-expressions: ( 43 | /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' 44 | /// ); 45 | /// 46 | $media-expressions: ( 47 | "screen": "screen", 48 | "print": "print", 49 | "handheld": "handheld", 50 | "landscape": "(orientation: landscape)", 51 | "portrait": "(orientation: portrait)", 52 | "retina2x": 53 | "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)", 54 | "retina3x": 55 | "(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)", 56 | ) !default; 57 | 58 | /// 59 | /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals 60 | /// 61 | /// @example scss - Interval for pixels is defined as `1` by default 62 | /// @include media('>128px') {} 63 | /// 64 | /// /* Generates: */ 65 | /// @media (min-width: 129px) {} 66 | /// 67 | /// @example scss - Interval for ems is defined as `0.01` by default 68 | /// @include media('>20em') {} 69 | /// 70 | /// /* Generates: */ 71 | /// @media (min-width: 20.01em) {} 72 | /// 73 | /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` 74 | /// @include media('>2.0rem') {} 75 | /// 76 | /// /* Generates: */ 77 | /// @media (min-width: 2.1rem) {} 78 | /// 79 | $unit-intervals: ( 80 | "px": 1, 81 | "em": 0.01, 82 | "rem": 0.1, 83 | "": 0, 84 | ) !default; 85 | 86 | /// 87 | /// Defines whether support for media queries is available, useful for creating separate stylesheets 88 | /// for browsers that don't support media queries. 89 | /// 90 | /// @example scss - Disables support for media queries 91 | /// $im-media-support: false; 92 | /// @include media('>=tablet') { 93 | /// .foo { 94 | /// color: tomato; 95 | /// } 96 | /// } 97 | /// 98 | /// /* Generates: */ 99 | /// .foo { 100 | /// color: tomato; 101 | /// } 102 | /// 103 | $im-media-support: true !default; 104 | 105 | /// 106 | /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or 107 | /// intercept the breakpoint will be displayed, any others will be ignored. 108 | /// 109 | /// @example scss - This media query will show because it intercepts the static breakpoint 110 | /// $im-media-support: false; 111 | /// $im-no-media-breakpoint: 'desktop'; 112 | /// @include media('>=tablet') { 113 | /// .foo { 114 | /// color: tomato; 115 | /// } 116 | /// } 117 | /// 118 | /// /* Generates: */ 119 | /// .foo { 120 | /// color: tomato; 121 | /// } 122 | /// 123 | /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint 124 | /// $im-media-support: false; 125 | /// $im-no-media-breakpoint: 'tablet'; 126 | /// @include media('>=desktop') { 127 | /// .foo { 128 | /// color: tomato; 129 | /// } 130 | /// } 131 | /// 132 | /// /* No output */ 133 | /// 134 | $im-no-media-breakpoint: "desktop" !default; 135 | 136 | /// 137 | /// Selects which media expressions are allowed in an expression for it to be used when media queries 138 | /// are not supported. 139 | /// 140 | /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions 141 | /// $im-media-support: false; 142 | /// $im-no-media-breakpoint: 'desktop'; 143 | /// $im-no-media-expressions: ('screen'); 144 | /// @include media('>=tablet', 'screen') { 145 | /// .foo { 146 | /// color: tomato; 147 | /// } 148 | /// } 149 | /// 150 | /// /* Generates: */ 151 | /// .foo { 152 | /// color: tomato; 153 | /// } 154 | /// 155 | /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted 156 | /// $im-media-support: false; 157 | /// $im-no-media-breakpoint: 'desktop'; 158 | /// $im-no-media-expressions: ('screen'); 159 | /// @include media('>=tablet', 'retina2x') { 160 | /// .foo { 161 | /// color: tomato; 162 | /// } 163 | /// } 164 | /// 165 | /// /* No output */ 166 | /// 167 | $im-no-media-expressions: ("screen", "portrait", "landscape") !default; 168 | 169 | //// 170 | /// Cross-engine logging engine 171 | /// @author Kitty Giraudel 172 | /// @access private 173 | //// 174 | 175 | /// 176 | /// Log a message either with `@error` if supported 177 | /// else with `@warn`, using `feature-exists('at-error')` 178 | /// to detect support. 179 | /// 180 | /// @param {String} $message - Message to log 181 | /// 182 | @function im-log($message) { 183 | @if feature-exists("at-error") { 184 | @error $message; 185 | } @else { 186 | @warn $message; 187 | $_: noop(); 188 | } 189 | 190 | @return $message; 191 | } 192 | 193 | /// 194 | /// Wrapper mixin for the log function so it can be used with a more friendly 195 | /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function 196 | /// within functions because it is not possible to include a mixin in a function 197 | /// and use the mixin everywhere else because it's much more elegant. 198 | /// 199 | /// @param {String} $message - Message to log 200 | /// 201 | @mixin log($message) { 202 | @if im-log($message) { 203 | } 204 | } 205 | 206 | /// 207 | /// Function with no `@return` called next to `@warn` in Sass 3.3 208 | /// to trigger a compiling error and stop the process. 209 | /// 210 | @function noop() { 211 | } 212 | 213 | /// 214 | /// Determines whether a list of conditions is intercepted by the static breakpoint. 215 | /// 216 | /// @param {Arglist} $conditions - Media query conditions 217 | /// 218 | /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint 219 | /// 220 | @function im-intercepts-static-breakpoint($conditions...) { 221 | $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); 222 | 223 | @if not $no-media-breakpoint-value { 224 | @if im-log("`#{$im-no-media-breakpoint}` is not a valid breakpoint.") { 225 | } 226 | } 227 | 228 | @each $condition in $conditions { 229 | @if not map-has-key($media-expressions, $condition) { 230 | $operator: get-expression-operator($condition); 231 | $prefix: get-expression-prefix($operator); 232 | $value: get-expression-value($condition, $operator); 233 | 234 | @if ($prefix == "max" and $value <= $no-media-breakpoint-value) or 235 | ($prefix == "min" and $value > $no-media-breakpoint-value) 236 | { 237 | @return false; 238 | } 239 | } @else if not index($im-no-media-expressions, $condition) { 240 | @return false; 241 | } 242 | } 243 | 244 | @return true; 245 | } 246 | 247 | //// 248 | /// Parsing engine 249 | /// @author Kitty Giraudel 250 | /// @access private 251 | //// 252 | 253 | /// 254 | /// Get operator of an expression 255 | /// 256 | /// @param {String} $expression - Expression to extract operator from 257 | /// 258 | /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` 259 | /// 260 | @function get-expression-operator($expression) { 261 | @each $operator in (">=", ">", "<=", "<", "≥", "≤") { 262 | @if str-index($expression, $operator) { 263 | @return $operator; 264 | } 265 | } 266 | 267 | // It is not possible to include a mixin inside a function, so we have to 268 | // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because 269 | // functions cannot be called anywhere in Sass, we need to hack the call in 270 | // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with 271 | // Sass 3.3, change this line in `@if im-log(..) {}` instead. 272 | $_: im-log("No operator found in `#{$expression}`."); 273 | } 274 | 275 | /// 276 | /// Get dimension of an expression, based on a found operator 277 | /// 278 | /// @param {String} $expression - Expression to extract dimension from 279 | /// @param {String} $operator - Operator from `$expression` 280 | /// 281 | /// @return {String} - `width` or `height` (or potentially anything else) 282 | /// 283 | @function get-expression-dimension($expression, $operator) { 284 | $operator-index: str-index($expression, $operator); 285 | $parsed-dimension: str-slice($expression, 0, $operator-index - 1); 286 | $dimension: "width"; 287 | 288 | @if str-length($parsed-dimension) > 0 { 289 | $dimension: $parsed-dimension; 290 | } 291 | 292 | @return $dimension; 293 | } 294 | 295 | /// 296 | /// Get dimension prefix based on an operator 297 | /// 298 | /// @param {String} $operator - Operator 299 | /// 300 | /// @return {String} - `min` or `max` 301 | /// 302 | @function get-expression-prefix($operator) { 303 | @return if(index(("<", "<=", "≤"), $operator), "max", "min"); 304 | } 305 | 306 | /// 307 | /// Get value of an expression, based on a found operator 308 | /// 309 | /// @param {String} $expression - Expression to extract value from 310 | /// @param {String} $operator - Operator from `$expression` 311 | /// 312 | /// @return {Number} - A numeric value 313 | /// 314 | @function get-expression-value($expression, $operator) { 315 | $operator-index: str-index($expression, $operator); 316 | $value: str-slice($expression, $operator-index + str-length($operator)); 317 | 318 | @if map-has-key($breakpoints, $value) { 319 | $value: map-get($breakpoints, $value); 320 | } @else { 321 | $value: to-number($value); 322 | } 323 | 324 | $interval: map-get($unit-intervals, unit($value)); 325 | 326 | @if not $interval { 327 | // It is not possible to include a mixin inside a function, so we have to 328 | // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because 329 | // functions cannot be called anywhere in Sass, we need to hack the call in 330 | // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with 331 | // Sass 3.3, change this line in `@if im-log(..) {}` instead. 332 | $_: im-log("Unknown unit `#{unit($value)}`."); 333 | } 334 | 335 | @if $operator == ">" { 336 | $value: $value + $interval; 337 | } @else if $operator == "<" { 338 | $value: $value - $interval; 339 | } 340 | 341 | @return $value; 342 | } 343 | 344 | /// 345 | /// Parse an expression to return a valid media-query expression 346 | /// 347 | /// @param {String} $expression - Expression to parse 348 | /// 349 | /// @return {String} - Valid media query 350 | /// 351 | @function parse-expression($expression) { 352 | // If it is part of $media-expressions, it has no operator 353 | // then there is no need to go any further, just return the value 354 | @if map-has-key($media-expressions, $expression) { 355 | @return map-get($media-expressions, $expression); 356 | } 357 | 358 | $operator: get-expression-operator($expression); 359 | $dimension: get-expression-dimension($expression, $operator); 360 | $prefix: get-expression-prefix($operator); 361 | $value: get-expression-value($expression, $operator); 362 | 363 | @return "(#{$prefix}-#{$dimension}: #{$value})"; 364 | } 365 | 366 | /// 367 | /// Slice `$list` between `$start` and `$end` indexes 368 | /// 369 | /// @access private 370 | /// 371 | /// @param {List} $list - List to slice 372 | /// @param {Number} $start [1] - Start index 373 | /// @param {Number} $end [length($list)] - End index 374 | /// 375 | /// @return {List} Sliced list 376 | /// 377 | @function slice($list, $start: 1, $end: length($list)) { 378 | @if length($list) < 1 or $start > $end { 379 | @return (); 380 | } 381 | 382 | $result: (); 383 | 384 | @for $i from $start through $end { 385 | $result: append($result, nth($list, $i)); 386 | } 387 | 388 | @return $result; 389 | } 390 | 391 | //// 392 | /// String to number converter 393 | /// @author Kitty Giraudel 394 | /// @access private 395 | //// 396 | 397 | /// 398 | /// Casts a string into a number 399 | /// 400 | /// @param {String | Number} $value - Value to be parsed 401 | /// 402 | /// @return {Number} 403 | /// 404 | @function to-number($value) { 405 | @if type-of($value) == "number" { 406 | @return $value; 407 | } @else if type-of($value) != "string" { 408 | $_: im-log("Value for `to-number` should be a number or a string."); 409 | } 410 | 411 | $first-character: str-slice($value, 1, 1); 412 | $result: 0; 413 | $digits: 0; 414 | $minus: ($first-character == "-"); 415 | $numbers: ( 416 | "0": 0, 417 | "1": 1, 418 | "2": 2, 419 | "3": 3, 420 | "4": 4, 421 | "5": 5, 422 | "6": 6, 423 | "7": 7, 424 | "8": 8, 425 | "9": 9, 426 | ); 427 | 428 | // Remove +/- sign if present at first character 429 | @if ($first-character == "+" or $first-character == "-") { 430 | $value: str-slice($value, 2); 431 | } 432 | 433 | @for $i from 1 through str-length($value) { 434 | $character: str-slice($value, $i, $i); 435 | 436 | @if not(index(map-keys($numbers), $character) or $character == ".") { 437 | @return to-length(if($minus, -$result, $result), str-slice($value, $i)); 438 | } 439 | 440 | @if $character == "." { 441 | $digits: 1; 442 | } @else if $digits == 0 { 443 | $result: $result * 10 + map-get($numbers, $character); 444 | } @else { 445 | $digits: $digits * 10; 446 | $result: $result + map-get($numbers, $character) / $digits; 447 | } 448 | } 449 | 450 | @return if($minus, -$result, $result); 451 | } 452 | 453 | /// 454 | /// Add `$unit` to `$value` 455 | /// 456 | /// @param {Number} $value - Value to add unit to 457 | /// @param {String} $unit - String representation of the unit 458 | /// 459 | /// @return {Number} - `$value` expressed in `$unit` 460 | /// 461 | @function to-length($value, $unit) { 462 | $units: ( 463 | "px": 1px, 464 | "cm": 1cm, 465 | "mm": 1mm, 466 | "%": 1%, 467 | "ch": 1ch, 468 | "pc": 1pc, 469 | "in": 1in, 470 | "em": 1em, 471 | "rem": 1rem, 472 | "pt": 1pt, 473 | "ex": 1ex, 474 | "vw": 1vw, 475 | "vh": 1vh, 476 | "vmin": 1vmin, 477 | "vmax": 1vmax, 478 | ); 479 | 480 | @if not index(map-keys($units), $unit) { 481 | $_: im-log("Invalid unit `#{$unit}`."); 482 | } 483 | 484 | @return $value * map-get($units, $unit); 485 | } 486 | 487 | /// 488 | /// This mixin aims at redefining the configuration just for the scope of 489 | /// the call. It is helpful when having a component needing an extended 490 | /// configuration such as custom breakpoints (referred to as tweakpoints) 491 | /// for instance. 492 | /// 493 | /// @author Kitty Giraudel 494 | /// 495 | /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` 496 | /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` 497 | /// 498 | /// @example scss - Extend the global breakpoints with a tweakpoint 499 | /// @include media-context(('custom': 678px)) { 500 | /// .foo { 501 | /// @include media('>phone', '<=custom') { 502 | /// // ... 503 | /// } 504 | /// } 505 | /// } 506 | /// 507 | /// @example scss - Extend the global media expressions with a custom one 508 | /// @include media-context($tweak-media-expressions: ('all': 'all')) { 509 | /// .foo { 510 | /// @include media('all', '>phone') { 511 | /// // ... 512 | /// } 513 | /// } 514 | /// } 515 | /// 516 | /// @example scss - Extend both configuration maps 517 | /// @include media-context(('custom': 678px), ('all': 'all')) { 518 | /// .foo { 519 | /// @include media('all', '>phone', '<=custom') { 520 | /// // ... 521 | /// } 522 | /// } 523 | /// } 524 | /// 525 | @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { 526 | // Save global configuration 527 | $global-breakpoints: $breakpoints; 528 | $global-media-expressions: $media-expressions; 529 | 530 | // Update global configuration 531 | $breakpoints: map-merge($breakpoints, $tweakpoints) !global; 532 | $media-expressions: map-merge( 533 | $media-expressions, 534 | $tweak-media-expressions 535 | ) !global; 536 | 537 | @content; 538 | 539 | // Restore global configuration 540 | $breakpoints: $global-breakpoints !global; 541 | $media-expressions: $global-media-expressions !global; 542 | } 543 | 544 | //// 545 | /// include-media public exposed API 546 | /// @author Eduardo Boucas 547 | /// @access public 548 | //// 549 | 550 | /// 551 | /// Generates a media query based on a list of conditions 552 | /// 553 | /// @param {Arglist} $conditions - Media query conditions 554 | /// 555 | /// @example scss - With a single set breakpoint 556 | /// @include media('>phone') { } 557 | /// 558 | /// @example scss - With two set breakpoints 559 | /// @include media('>phone', '<=tablet') { } 560 | /// 561 | /// @example scss - With custom values 562 | /// @include media('>=358px', '<850px') { } 563 | /// 564 | /// @example scss - With set breakpoints with custom values 565 | /// @include media('>desktop', '<=1350px') { } 566 | /// 567 | /// @example scss - With a static expression 568 | /// @include media('retina2x') { } 569 | /// 570 | /// @example scss - Mixing everything 571 | /// @include media('>=350px', ' 0) { 579 | @media #{unquote(parse-expression(nth($conditions, 1)))} { 580 | // Recursive call 581 | $sliced-conditions: slice($conditions, 2); 582 | @include media($sliced-conditions...) { 583 | @content; 584 | } 585 | } 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /styles/_vendor/_lenis.scss: -------------------------------------------------------------------------------- 1 | html.lenis { 2 | height: auto; 3 | } 4 | 5 | .lenis.lenis-smooth { 6 | scroll-behavior: auto; 7 | } 8 | 9 | .lenis.lenis-smooth [data-lenis-prevent] { 10 | overscroll-behavior: contain; 11 | } 12 | 13 | .lenis.lenis-stopped { 14 | overflow: hidden; 15 | } 16 | 17 | .lenis.lenis-scrolling iframe { 18 | pointer-events: none; 19 | } 20 | -------------------------------------------------------------------------------- /styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "_vendor/include-media"; 2 | @import "_vendor/lenis"; 3 | 4 | @import "_abstracts/variables"; 5 | @import "_abstracts/functions"; 6 | @import "_abstracts/mixins"; 7 | 8 | @import "_base/reset"; 9 | @import "_base/fonts"; 10 | @import "_base/globals"; 11 | 12 | @import "_components/hello"; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "strictPropertyInitialization": false 6 | } 7 | } 8 | --------------------------------------------------------------------------------