61 |
62 |
{
65 | thumbRef.current = node
66 | thumbMeasureRef(node)
67 | }}
68 | />
69 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/studio-freight/bibliotheca)
2 |
3 |
18 |
19 | ## Introduction
20 |
21 | Compono is our Next.js/React component library.
22 |
23 | 🚧 This package is not stable, API might change at any time 🚧
24 |
25 |
26 |
27 | ## Composition
28 |
29 | This component kit is composed of:
30 |
31 |
32 |
33 | ## Considerations/Requirements
34 |
35 | This component kit is meant to be used in a nextjs environment that also supports sass, this component kit is not transpiled, make sure you add it to the transpilePackages array in your next.js config:
36 |
37 | ```
38 | transpilePackages: ['@studio-freight/compono'],
39 | ```
40 |
41 |
42 |
43 | ## Folder Structure
44 |
45 | - **/src:** @studio-freight/compono's raw code.
46 |
47 |
48 |
49 | ## Compono in use
50 |
51 | - [@studio-freight/satus](https://github.com/studio-freight/satus) Our starter kit.
52 |
53 |
54 |
55 | ## Authors
56 |
57 | This toolkit is curated and maintained by the Studio Freight Darkroom team:
58 |
59 | - Clement Roche ([@clementroche\_](https://twitter.com/clementroche_)) – [Studio Freight](https://studiofreight.com)
60 | - Guido Fier ([@uido15](https://twitter.com/uido15)) – [Studio Freight](https://studiofreight.com)
61 | - Leandro Soengas ([@lsoengas](https://twitter.com/lsoengas)) - [Studio Freight](https://studiofreight.com)
62 | - Franco Arza ([@arzafran](https://twitter.com/arzafran)) - [Studio Freight](https://studiofreight.com)
63 |
64 |
65 |
66 | ## License
67 |
68 | [The MIT License.](https://opensource.org/licenses/MIT)
69 |
--------------------------------------------------------------------------------
/src/media/lottie/index.js:
--------------------------------------------------------------------------------
1 | import { useDocumentReadyState, useIntersectionObserver } from '@studio-freight/hamo'
2 | import cn from 'clsx'
3 | import { useEffect, useRef, useState } from 'react'
4 | import s from './lottie.module.scss'
5 |
6 | export function Lottie({ animation, speed = 1, loop = true, className, viewThreshold = 0, id = '' }) {
7 | const lottieRef = useRef(null)
8 | const animator = useRef(null)
9 | const [lottie, setLottie] = useState()
10 |
11 | const [setRef, { isIntersecting }] = useIntersectionObserver({
12 | threshold: viewThreshold,
13 | })
14 | const readyState = useDocumentReadyState()
15 |
16 | const fetchJson = async (externalAnimation) => {
17 | const response = await fetch(externalAnimation)
18 | const json = await response.json()
19 | return json
20 | }
21 |
22 | useEffect(() => {
23 | if (readyState === 'complete') {
24 | //needed to render on 'svg'
25 | import('lottie-web/build/player/lottie.min').then((Lottie) => setLottie(Lottie.default))
26 | }
27 | }, [readyState])
28 |
29 | useEffect(() => {
30 | if (!lottie) return
31 |
32 | if (typeof animation === 'string') {
33 | // external link or json file in public folder
34 | fetchJson(animation).then((json) => {
35 | animator.current = lottie?.loadAnimation({
36 | container: lottieRef.current,
37 | animationData: json,
38 | renderer: animation.includes('http') ? 'svg' : 'canvas',
39 | autoplay: false,
40 | loop,
41 | name: id,
42 | })
43 | })
44 | } else {
45 | // json data inside code
46 | animator.current = lottie?.loadAnimation({
47 | container: lottieRef.current,
48 | animationData: animation,
49 | renderer: 'canvas',
50 | autoplay: false,
51 | loop,
52 | name: id,
53 | })
54 | }
55 |
56 | animator.current?.setSpeed(speed)
57 | return () => animator.current?.destroy()
58 | }, [lottie])
59 |
60 | useEffect(() => {
61 | if (animator.current && isIntersecting) {
62 | animator.current?.play()
63 | } else {
64 | animator.current?.pause()
65 | }
66 | }, [animator.current, isIntersecting])
67 |
68 | return (
69 |
{
73 | lottieRef.current = node
74 | setRef(node)
75 | }}
76 | />
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/slider/index.js:
--------------------------------------------------------------------------------
1 | import cn from 'clsx'
2 | import Autoplay from 'embla-carousel-autoplay'
3 | import useEmblaCarousel from 'embla-carousel-react'
4 | import { createContext, useCallback, useContext, useEffect, useState } from 'react'
5 | import s from './slider.module.scss'
6 |
7 | //this folder is temp
8 |
9 | const SliderContext = createContext({})
10 |
11 | export function useSlider() {
12 | return useContext(SliderContext)
13 | }
14 |
15 | export const Slider = ({ children, emblaApi = { autoplay: false }, enabled = true, customProps = {} }) => {
16 | const [currentIndex, setCurrentIndex] = useState(0)
17 | const [scrollProgress, setScrollProgress] = useState(0)
18 | const autoplay = Autoplay({ delay: emblaApi?.autoplay?.delay || null }, (emblaRoot) => emblaRoot.parentElement)
19 | const [emblaRef, embla] = useEmblaCarousel(emblaApi, emblaApi.autoplay ? [autoplay] : [])
20 |
21 | const scrollPrev = useCallback(() => {
22 | embla && embla.scrollPrev()
23 | }, [embla])
24 |
25 | const scrollNext = useCallback(() => {
26 | embla && embla.scrollNext()
27 | }, [embla])
28 |
29 | const scrollTo = useCallback((index) => embla && embla.scrollTo(index), [embla])
30 |
31 | const getScrollProgress = useCallback(() => {
32 | embla && setScrollProgress(Math.max(0, Math.min(1, embla.scrollProgress())))
33 | }, [embla])
34 |
35 | const getScrollSnap = useCallback(() => {
36 | setCurrentIndex(embla.selectedScrollSnap())
37 | }, [embla])
38 |
39 | useEffect(() => {
40 | if (embla) {
41 | getScrollSnap()
42 | getScrollProgress()
43 | embla.on('select', getScrollSnap)
44 | embla.on('scroll', getScrollProgress)
45 | embla.on('reInit', getScrollProgress)
46 | }
47 | }, [embla])
48 |
49 | useEffect(() => {
50 | if (!enabled && embla) {
51 | embla.destroy()
52 | }
53 | }, [embla, enabled])
54 |
55 | return (
56 |
68 | {children}
69 |
70 | )
71 | }
72 |
73 | const Slides = ({ children, className }) => {
74 | const { emblaRef } = useSlider()
75 |
76 | return (
77 |
80 | )
81 | }
82 |
83 | Slider.Slides = Slides
84 |
--------------------------------------------------------------------------------
/src/media/index.js:
--------------------------------------------------------------------------------
1 | import { Image } from './image'
2 | import { Lottie } from './lottie'
3 | import { Video } from './video'
4 | import { useState } from 'react'
5 |
6 | const extensions = [
7 | {
8 | type: 'IsImage',
9 | extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
10 | },
11 | { type: 'IsVideo', extensions: ['mp4', 'webm', 'ogg', 'vimeo', 'youtu'] },
12 | { type: 'IsLottie', extensions: ['json'] },
13 | ]
14 |
15 | const belongsTo = (extension) =>
16 | extensions.find(({ extensions }) => extensions.some((filetype) => extension.includes(filetype)))?.type
17 |
18 | export const Media = ({ className, media, onLoad, ...props }) => {
19 | if (!media?.source) return null
20 |
21 | const extension = belongsTo(media.source)
22 |
23 | if (extension) {
24 | return options[extension]({ className, media, onLoad, ...props })
25 | }
26 |
27 | console.warn('media not supported, please check if extension is correct or should be added to media-switch component')
28 |
29 | return null
30 | }
31 |
32 | const options = {
33 | IsImage: function ({ className, media, onLoad, style = {}, ...props }) {
34 | return (
35 |
{
43 | onLoad?.(e.currentTarget.currentSrc)
44 | }}
45 | />
46 | )
47 | },
48 | IsLottie: function ({ className, media, ...props }) {
49 | const animation = media.source
50 |
51 | return
52 | },
53 | IsVideo: function ({ className, media, onLoad, poster, ...props }) {
54 | const { embedded, fill, priority, ...mapProps } = props
55 | const [loaded, setLoaded] = useState()
56 |
57 | return (
58 |
59 | {poster && (
60 |
68 | )}
69 |
80 | )
81 | },
82 | }
83 |
--------------------------------------------------------------------------------
/website/styles/_layout.scss:
--------------------------------------------------------------------------------
1 | // inspiration: https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins
2 |
3 | // css variables exposed globally:
4 | // --layout-columns-count: columns count in the layout
5 | // --layout-columns-gap: gap size between columns
6 | // --layout-margin: layout margin size (left or right)
7 | // --layout-width: 100vw minus 2 * --layout-margin
8 | // --layout-column-width: size of a single column
9 |
10 | // css classes exposed globally:
11 | // .layout-block: element takes the whole layout width
12 | // .layout-block-inner: same as .layout-block but using padding instead of margin
13 | // .layout-grid: extends .layout-block with grid behaviour using layout settings
14 | // .layout-grid-inner: same as .layout-grid but using padding instead of margin
15 |
16 | @use 'sass:map';
17 |
18 | // config to fill
19 | // 'variable': (mobile, desktop)
20 | $layout: (
21 | 'columns-count': (
22 | 4,
23 | 12,
24 | ),
25 | 'columns-gap': (
26 | 10px,
27 | 25px,
28 | ),
29 | 'margin': (
30 | 20px,
31 | 50px,
32 | ),
33 | );
34 |
35 | //internal process, do not touch
36 | :root {
37 | --layout-columns-count: #{nth(map.get($layout, 'columns-count'), 1)};
38 | --layout-columns-gap: #{mobile-vw(nth(map.get($layout, 'columns-gap'), 1))};
39 | --layout-margin: #{mobile-vw(nth(map.get($layout, 'margin'), 1))};
40 |
41 | @include desktop {
42 | --layout-columns-count: #{nth(map.get($layout, 'columns-count'), 2)};
43 | --layout-columns-gap: #{desktop-vw(nth(map.get($layout, 'columns-gap'), 2))};
44 | --layout-margin: #{desktop-vw(nth(map.get($layout, 'margin'), 2))};
45 | }
46 |
47 | --layout-width: calc(100vw - (2 * var(--layout-margin)));
48 | --layout-column-width: calc(
49 | (
50 | var(--layout-width) -
51 | ((var(--layout-columns-count) - 1) * var(--layout-columns-gap))
52 | ) / var(--layout-columns-count)
53 | );
54 | }
55 |
56 | .layout-block {
57 | max-width: var(--layout-width);
58 | margin-left: auto;
59 | margin-right: auto;
60 | width: 100%;
61 | }
62 |
63 | .layout-block-inner {
64 | padding-left: var(--layout-margin);
65 | padding-right: var(--layout-margin);
66 | width: 100%;
67 | }
68 |
69 | .layout-grid {
70 | @extend .layout-block;
71 |
72 | display: grid;
73 | grid-template-columns: repeat(var(--layout-columns-count), minmax(0, 1fr));
74 | grid-gap: var(--layout-columns-gap);
75 | width: 100%;
76 | }
77 |
78 | .layout-grid-inner {
79 | @extend .layout-block-inner;
80 |
81 | display: grid;
82 | grid-template-columns: repeat(var(--layout-columns-count), minmax(0, 1fr));
83 | grid-gap: var(--layout-columns-gap);
84 | width: 100%;
85 | }
86 |
--------------------------------------------------------------------------------
/website/libs/theatre/index.js:
--------------------------------------------------------------------------------
1 | import { useFrame } from '@studio-freight/hamo'
2 | import { createRafDriver, getProject } from '@theatre/core'
3 | import {
4 | createContext,
5 | forwardRef,
6 | useContext,
7 | useEffect,
8 | useImperativeHandle,
9 | useMemo,
10 | useRef,
11 | useState,
12 | } from 'react'
13 |
14 | const RafDriverContext = createContext()
15 |
16 | export function RafDriverProvider({ children, id = 'default' }) {
17 | const [theatreRaf] = useState(() => createRafDriver({ name: id }))
18 |
19 | useFrame((time) => {
20 | theatreRaf.tick(time)
21 | })
22 |
23 | return (
24 |
25 | {children}
26 |
27 | )
28 | }
29 |
30 | export function useCurrentRafDriver() {
31 | return useContext(RafDriverContext)
32 | }
33 |
34 | const ProjectContext = createContext()
35 |
36 | export function ProjectProvider({ children, id, config }) {
37 | const [project, setproject] = useState()
38 | const isLoadingRef = useRef(false)
39 |
40 | useEffect(() => {
41 | if (config) {
42 | if (!isLoadingRef.current) {
43 | isLoadingRef.current = true
44 | fetch(config)
45 | .then((response) => response.json())
46 | .then((state) => {
47 | const project = getProject(id, { state })
48 |
49 | project.ready.then(() => {
50 | console.log('project ready')
51 | setproject(project)
52 | })
53 | })
54 | }
55 | } else {
56 | const project = getProject(id)
57 |
58 | project.ready.then(() => {
59 | setproject(project)
60 | })
61 | }
62 | }, [config, id])
63 |
64 | return (
65 |
66 | {children}
67 |
68 | )
69 | }
70 |
71 | export function useCurrentProject() {
72 | return useContext(ProjectContext)
73 | }
74 |
75 | const SheetContext = createContext()
76 |
77 | export function useSheet(id, instance) {
78 | const project = useCurrentProject()
79 |
80 | const sheet = useMemo(
81 | () => project?.sheet(id, instance),
82 | [project, id, instance]
83 | )
84 |
85 | return sheet
86 | }
87 |
88 | export const SheetProvider = forwardRef(function SheetProvider(
89 | { children, id, instance },
90 | ref
91 | ) {
92 | const sheet = useSheet(id, instance)
93 |
94 | useImperativeHandle(ref, () => sheet, [sheet])
95 |
96 | return {children}
97 | })
98 |
99 | export function useCurrentSheet() {
100 | return useContext(SheetContext)
101 | }
102 |
--------------------------------------------------------------------------------
/website/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { RealViewport } from '@studio-freight/compono'
2 | import { useLenis } from '@studio-freight/react-lenis'
3 | import Tempus from '@studio-freight/tempus'
4 | import { DeviceDetectionProvider } from 'components/device-detection'
5 | import { gsap } from 'gsap'
6 | import { ScrollTrigger } from 'gsap/dist/ScrollTrigger'
7 | import { GTM_ID } from 'libs/analytics'
8 | import { Orchestra } from 'libs/orchestra'
9 | import { useStore } from 'libs/store'
10 | import { ProjectProvider, RafDriverProvider } from 'libs/theatre'
11 | import Script from 'next/script'
12 | import { useEffect } from 'react'
13 | import 'styles/global.scss'
14 |
15 | if (typeof window !== 'undefined') {
16 | gsap.defaults({ ease: 'none' })
17 | gsap.registerPlugin(ScrollTrigger)
18 | ScrollTrigger.defaults({ markers: process.env.NODE_ENV === 'development' })
19 |
20 | // merge rafs
21 | gsap.ticker.lagSmoothing(0)
22 | gsap.ticker.remove(gsap.updateRoot)
23 | Tempus?.add((time) => {
24 | gsap.updateRoot(time / 1000)
25 | }, 0)
26 |
27 | // reset scroll position
28 | window.scrollTo(0, 0)
29 | window.history.scrollRestoration = 'manual'
30 | }
31 |
32 | function MyApp({ Component, pageProps }) {
33 | const lenis = useLenis(ScrollTrigger.update)
34 | useEffect(ScrollTrigger.refresh, [lenis])
35 |
36 | const navIsOpened = useStore(({ navIsOpened }) => navIsOpened)
37 |
38 | useEffect(() => {
39 | if (navIsOpened) {
40 | lenis?.stop()
41 | } else {
42 | lenis?.start()
43 | }
44 | }, [lenis, navIsOpened])
45 |
46 | return (
47 | <>
48 | {/* Google Tag Manager - Global base code */}
49 | {process.env.NODE_ENV !== 'development' && (
50 | <>
51 |
56 |
66 | >
67 | )}
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | >
81 | )
82 | }
83 |
84 | export default MyApp
85 |
--------------------------------------------------------------------------------
/website/libs/sass-utils/index.js:
--------------------------------------------------------------------------------
1 | // https://github.com/sass-eyeglass/node-sass-utils/blob/master/lib/coercion.js
2 |
3 | const sass = require('sass')
4 | const isSassType = require('./util').isSassType
5 |
6 | function hexToRGB(hex) {
7 | if (!/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex))
8 | throw new Error(`Invalid hex: ${hex}`)
9 |
10 | let c = hex.substring(1).split('')
11 | if (c.length == 3) {
12 | c = [c[0], c[0], c[1], c[1], c[2], c[2]]
13 | }
14 | c = `0xff${c.join('')}`
15 | return Number(c)
16 | }
17 |
18 | function castToSass(jsValue) {
19 | if (jsValue && typeof jsValue.toSass === 'function') {
20 | // string -> unquoted string
21 | return jsValue.toSass()
22 | } else if (typeof jsValue === 'string') {
23 | // string -> unquoted string
24 |
25 | if (jsValue.includes('px')) {
26 | return new sass.types.Number(Number(jsValue.replace('px', '')), 'px')
27 | } else if (jsValue.startsWith('#')) {
28 | //TODO: accept rgb values
29 | return new sass.types.Color(hexToRGB(jsValue))
30 | } else {
31 | return new sass.types.String(jsValue)
32 | }
33 | } else if (typeof jsValue === 'boolean') {
34 | // boolean -> boolean
35 | return jsValue ? sass.types.Boolean.TRUE : sass.types.Boolean.FALSE
36 | } else if (typeof jsValue === 'undefined' || jsValue === null) {
37 | // undefined/null -> null
38 | return sass.types.Null.NULL
39 | } else if (typeof jsValue === 'number') {
40 | // Js Number -> Unitless Number
41 | return new sass.types.Number(jsValue)
42 | } else if (jsValue && jsValue.constructor.name === 'Array') {
43 | // Array -> List
44 | var list = new sass.types.List(jsValue.length)
45 | for (var i = 0; i < jsValue.length; i++) {
46 | list.setValue(i, this.castToSass(jsValue[i]))
47 | }
48 | var isComma =
49 | typeof jsValue.separator === 'undefined' ? true : jsValue.separator
50 | list.setSeparator(isComma)
51 | return list
52 | } else if (jsValue === sass.types.Null.NULL) {
53 | // no-op if sass.types.Null.NULL
54 | return jsValue
55 | } else if (jsValue && isSassType(jsValue)) {
56 | // these are sass objects that we don't coerce
57 | return jsValue
58 | } else if (typeof jsValue === 'object') {
59 | var keys = []
60 | for (var k in jsValue) {
61 | if (jsValue.hasOwnProperty(k)) {
62 | keys[keys.length] = k
63 | }
64 | }
65 | var map = new sass.types.Map(keys.length)
66 | for (var m = 0; m < keys.length; m++) {
67 | var key = keys[m]
68 | map.setKey(m, new sass.types.String(key))
69 | map.setValue(m, this.castToSass(jsValue[key]))
70 | }
71 | return map
72 | } else {
73 | // WTF
74 | throw new Error("Don't know how to coerce: " + jsValue.toString())
75 | }
76 | }
77 |
78 | module.exports = {
79 | castToSass,
80 | }
81 |
--------------------------------------------------------------------------------
/website/styles/_reset.scss:
--------------------------------------------------------------------------------
1 | /***
2 | The new CSS reset - version 1.9 (last updated 19.6.2023)
3 | GitHub page: https://github.com/elad2412/the-new-css-reset
4 | ***/
5 |
6 | /*
7 | Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
8 | - The "symbol *" part is to solve Firefox SVG sprite bug
9 | - The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
10 | */
11 | *:where(
12 | :not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)
13 | ) {
14 | all: unset;
15 | display: revert;
16 | }
17 |
18 | /* Preferred box-sizing value */
19 | *,
20 | *::before,
21 | *::after {
22 | box-sizing: border-box;
23 | }
24 |
25 | /* Reapply the pointer cursor for anchor tags */
26 | a,
27 | button {
28 | cursor: revert;
29 | }
30 |
31 | /* Remove list styles (bullets/numbers) */
32 | ol,
33 | ul,
34 | menu {
35 | list-style: none;
36 | }
37 |
38 | /* For images to not be able to exceed their container */
39 | img {
40 | max-inline-size: 100%;
41 | max-block-size: 100%;
42 | }
43 |
44 | /* removes spacing between cells in tables */
45 | table {
46 | border-collapse: collapse;
47 | }
48 |
49 | /* Safari - solving issue when using user-select:none on the text input doesn't working */
50 | input,
51 | textarea {
52 | -webkit-user-select: auto;
53 | }
54 |
55 | /* revert the 'white-space' property for textarea elements on Safari */
56 | textarea {
57 | white-space: revert;
58 | }
59 |
60 | /* minimum style to allow to style meter element */
61 | meter {
62 | -webkit-appearance: revert;
63 | appearance: revert;
64 | }
65 |
66 | /* preformatted text - use only for this feature */
67 | :where(pre) {
68 | all: revert;
69 | }
70 |
71 | /* reset default text opacity of input placeholder */
72 | ::placeholder {
73 | color: unset;
74 | }
75 |
76 | /* remove default dot (•) sign */
77 | ::marker {
78 | content: initial;
79 | }
80 |
81 | /* fix the feature of 'hidden' attribute.
82 | display:revert; revert to element instead of attribute */
83 | :where([hidden]) {
84 | display: none;
85 | }
86 |
87 | /* revert for bug in Chromium browsers
88 | - fix for the content editable attribute will work properly.
89 | - webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
90 | :where([contenteditable]:not([contenteditable='false'])) {
91 | -moz-user-modify: read-write;
92 | -webkit-user-modify: read-write;
93 | overflow-wrap: break-word;
94 | -webkit-line-break: after-white-space;
95 | -webkit-user-select: auto;
96 | }
97 |
98 | /* apply back the draggable feature - exist only in Chromium and Safari */
99 | :where([draggable='true']) {
100 | -webkit-user-drag: element;
101 | }
102 |
103 | /* Revert Modal native behavior */
104 | :where(dialog:modal) {
105 | all: revert;
106 | }
107 |
--------------------------------------------------------------------------------
/src/cursor/index.js:
--------------------------------------------------------------------------------
1 | import cn from 'clsx'
2 | import gsap from 'gsap'
3 | import { useCallback, useEffect, useRef, useState } from 'react'
4 | import s from './cursor.module.scss'
5 |
6 | function Cursor() {
7 | const cursor = useRef()
8 | const [isGrab, setIsGrab] = useState(false)
9 | const [isPointer, setIsPointer] = useState(false)
10 | const [hasMoved, setHasMoved] = useState(false)
11 |
12 | const onMouseMove = useCallback(
13 | ({ clientX, clientY }) => {
14 | gsap.to(cursor.current, {
15 | x: clientX,
16 | y: clientY,
17 | duration: hasMoved ? 0.6 : 0,
18 | ease: 'expo.out',
19 | })
20 | setHasMoved(true)
21 | },
22 | [hasMoved]
23 | )
24 |
25 | useEffect(() => {
26 | window.addEventListener('mousemove', onMouseMove, false)
27 |
28 | return () => {
29 | window.removeEventListener('mousemove', onMouseMove, false)
30 | }
31 | }, [hasMoved, onMouseMove])
32 |
33 | useEffect(() => {
34 | document.documentElement.classList.add('has-custom-cursor')
35 |
36 | return () => {
37 | document.documentElement.classList.remove('has-custom-cursor')
38 | }
39 | }, [])
40 |
41 | useEffect(() => {
42 | let elements = []
43 |
44 | const onMouseEnter = () => {
45 | setIsPointer(true)
46 | }
47 | const onMouseLeave = () => {
48 | setIsPointer(false)
49 | }
50 |
51 | elements = [...document.querySelectorAll("button,a,input,label,[data-cursor='pointer']")]
52 |
53 | elements.forEach((element) => {
54 | element.addEventListener('mouseenter', onMouseEnter, false)
55 | element.addEventListener('mouseleave', onMouseLeave, false)
56 | })
57 |
58 | return () => {
59 | elements.forEach((element) => {
60 | element.removeEventListener('mouseenter', onMouseEnter, false)
61 | element.removeEventListener('mouseleave', onMouseLeave, false)
62 | })
63 | }
64 | }, [])
65 |
66 | useEffect(() => {
67 | let elements = []
68 |
69 | const onMouseEnter = () => {
70 | setIsGrab(true)
71 | }
72 | const onMouseLeave = () => {
73 | setIsGrab(false)
74 | }
75 |
76 | elements = [...document.querySelectorAll("button,a,input,label,[data-cursor='pointer']")]
77 |
78 | elements.forEach((element) => {
79 | element.addEventListener('mouseenter', onMouseEnter, false)
80 | element.addEventListener('mouseleave', onMouseLeave, false)
81 | })
82 |
83 | return () => {
84 | elements.forEach((element) => {
85 | element.removeEventListener('mouseenter', onMouseEnter, false)
86 | element.removeEventListener('mouseleave', onMouseLeave, false)
87 | })
88 | }
89 | }, [])
90 |
91 | return (
92 |
97 | )
98 | }
99 |
100 | export { Cursor }
101 |
--------------------------------------------------------------------------------
/src/transparent-video/index.js:
--------------------------------------------------------------------------------
1 | import s from './transparent-video.module.scss'
2 |
3 | // export function TransparentVideo({ colorVideoSrc, alphaMaskSrc }) {
4 | // const colorVideoRef = useRef(null)
5 | // const alphaMaskRef = useRef(null)
6 | // const canvasRef = useRef(null)
7 |
8 | // useEffect(() => {
9 | // const colorVideo = colorVideoRef.current
10 | // const alphaMask = alphaMaskRef.current
11 | // const canvas = canvasRef.current
12 | // const ctx = canvas.getContext('2d')
13 |
14 | // let animationFrameId
15 |
16 | // const render = () => {
17 | // canvas.width = colorVideo.videoWidth
18 | // canvas.height = colorVideo.videoHeight
19 |
20 | // // Draw the alpha mask video frame on an offscreen canvas
21 | // const offscreenCanvas = new OffscreenCanvas(colorVideo.videoWidth, colorVideo.videoHeight)
22 | // const offscreenCtx = offscreenCanvas.getContext('2d')
23 | // offscreenCtx.drawImage(alphaMask, 0, 0)
24 |
25 | // // Get the image data from the alpha mask offscreen canvas
26 | // const alphaData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height).data
27 |
28 | // // Draw the color video frame on the canvas
29 | // ctx.drawImage(colorVideo, 0, 0)
30 |
31 | // // Get the image data from the color video frame
32 | // const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
33 | // const data = imageData.data
34 |
35 | // // Apply the alpha mask to the color video frame
36 | // for (let i = 0, len = data.length; i < len; i += 4) {
37 | // data[i + 3] = alphaData[i]
38 | // }
39 |
40 | // // Put the processed image data back onto the canvas
41 | // ctx.putImageData(imageData, 0, 0)
42 |
43 | // animationFrameId = requestAnimationFrame(render)
44 | // }
45 |
46 | // const startRendering = () => {
47 | // if (
48 | // colorVideo.readyState === colorVideo.HAVE_ENOUGH_DATA &&
49 | // alphaMask.readyState === alphaMask.HAVE_ENOUGH_DATA
50 | // ) {
51 | // colorVideo.play()
52 | // alphaMask.play()
53 | // render()
54 | // }
55 | // }
56 |
57 | // colorVideo.addEventListener('canplay', startRendering)
58 | // alphaMask.addEventListener('canplay', startRendering)
59 |
60 | // return () => {
61 | // cancelAnimationFrame(animationFrameId)
62 | // colorVideo.removeEventListener('canplay', startRendering)
63 | // alphaMask.removeEventListener('canplay', startRendering)
64 | // }
65 | // }, [])
66 |
67 | // return (
68 | // <>
69 | //
70 | //
71 | //
72 | // >
73 | // )
74 | // }
75 |
76 | export function TransparentVideo({ src, alphaMask }) {
77 | return (
78 |
79 |
80 |
81 |
82 | )
83 | }
84 |
85 | export default TransparentVideo
86 |
--------------------------------------------------------------------------------
/website/libs/orchestra/index.js:
--------------------------------------------------------------------------------
1 | import { del, get, set } from 'idb-keyval'
2 | // import { Studio } from 'libs/theatre/studio'
3 | import { broadcast } from 'libs/zustand-broadcast'
4 | import dynamic from 'next/dynamic'
5 | import { useEffect } from 'react'
6 | import { create } from 'zustand'
7 | import { createJSONStorage, persist } from 'zustand/middleware'
8 | import { shallow } from 'zustand/shallow'
9 | import s from './orchestra.module.scss'
10 |
11 | const Studio = dynamic(
12 | () => import('libs/theatre/studio').then(({ Studio }) => Studio),
13 | { ssr: false }
14 | )
15 | const Stats = dynamic(() => import('./stats').then(({ Stats }) => Stats), {
16 | ssr: false,
17 | })
18 | const GridDebugger = dynamic(
19 | () => import('./grid').then(({ GridDebugger }) => GridDebugger),
20 | {
21 | ssr: false,
22 | }
23 | )
24 |
25 | // avoid to display debug tools on orchestra page
26 | const useInternalStore = create((set) => ({
27 | isVisible: true,
28 | setIsVisible: (isVisible) => set({ isVisible }),
29 | }))
30 |
31 | // https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md
32 | const INDEXEDDB_STORAGE = {
33 | getItem: async (name) => {
34 | // console.log(name, 'has been retrieved')
35 | return (await get(name)) || null
36 | },
37 | setItem: async (name, value) => {
38 | // console.log(name, 'with value', value, 'has been saved')
39 | await set(name, value)
40 | },
41 | removeItem: async (name) => {
42 | // console.log(name, 'has been deleted')
43 | await del(name)
44 | },
45 | }
46 |
47 | export const useOrchestraStore = create(
48 | persist(
49 | () => ({
50 | studio: false,
51 | stats: false,
52 | grid: false,
53 | }),
54 | {
55 | name: 'orchestra',
56 | storage: createJSONStorage(() => INDEXEDDB_STORAGE),
57 | }
58 | )
59 | )
60 |
61 | broadcast(useOrchestraStore, 'orchestra')
62 |
63 | export function Orchestra() {
64 | const isVisible = useInternalStore(({ isVisible }) => isVisible)
65 |
66 | const { studio, stats, grid } = useOrchestraStore(
67 | ({ studio, stats, grid }) => ({ studio, stats, grid }),
68 | shallow
69 | )
70 |
71 | return (
72 | isVisible && (
73 | <>
74 | {studio && }
75 | {stats && }
76 | {grid && }
77 | >
78 | )
79 | )
80 | }
81 |
82 | // to be added to debug pages
83 | export function OrchestraToggle() {
84 | useEffect(() => {
85 | useInternalStore.setState(() => ({
86 | isVisible: false,
87 | }))
88 | }, [])
89 |
90 | return (
91 |
92 |
99 |
106 |
113 |
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/src/page-transition/index.js:
--------------------------------------------------------------------------------
1 | import { useLenis } from '@studio-freight/react-lenis'
2 | import { createContext, useCallback, useEffect, useRef, useState } from 'react'
3 |
4 | export const PageTransitionContext = createContext()
5 |
6 | export function PageTransition({ Component, pageProps }) {
7 | const current = useRef()
8 | const next = useRef()
9 |
10 | const [components, setComponents] = useState(() => [{ ...Component, pageProps }])
11 | const [lifecycle, setLifecycle] = useState('starting')
12 | const [isTransitioning, setIsTransitioning] = useState(false)
13 |
14 | useEffect(() => {
15 | if (!Component) return
16 |
17 | const currentPage = components[0].pageProps.id || components[0].render?.displayName
18 | const nextPage = pageProps.id || Component.render?.displayName
19 |
20 | if (currentPage === nextPage) return
21 |
22 | setComponents((data) => [data[0], { ...Component, pageProps }])
23 | }, [Component, pageProps])
24 |
25 | const lenis = useLenis()
26 |
27 | const onAnimationEnded = useCallback(
28 | ({ scrollTo }) => {
29 | if (scrollTo) {
30 | lenis.scrollTo(0, { immediate: true, force: true })
31 | }
32 | current.current = next.current
33 | next.current = null
34 | if (components.length > 1) return setComponents((data) => [data[1]])
35 | if (components.length === 1) return setLifecycle('resting')
36 | },
37 | [lenis, components]
38 | )
39 |
40 | const onAnimationStart = useCallback(async () => {
41 | const from = components[0]?.render?.displayName
42 | const to = components[1]?.render?.displayName
43 |
44 | if (components.length === 1) return
45 |
46 | setIsTransitioning(true)
47 | lenis.stop()
48 |
49 | if (next?.current?.preAnimateIn) {
50 | await next.current.preAnimateIn({ from, to })
51 | }
52 | if (current?.current?.animateOut) {
53 | await current.current.animateOut({ from, to })
54 | }
55 |
56 | onAnimationEnded({ scrollTo: true })
57 |
58 | setIsTransitioning(false)
59 |
60 | if (current?.current?.animateIn) {
61 | await current.current.animateIn({ from, to })
62 | }
63 |
64 | lenis.start()
65 | }, [lenis, components, onAnimationEnded])
66 |
67 | useEffect(() => {
68 | if (components.length === 1 && lifecycle !== 'starting') return setLifecycle('resting')
69 |
70 | if (lifecycle === 'transitioning' || components.length < 1) return
71 |
72 | setLifecycle('transitioning')
73 |
74 | onAnimationStart()
75 | }, [components, onAnimationStart])
76 |
77 | const getPosition = useCallback(
78 | (i) => {
79 | if (components.length > 1) {
80 | return i === 0
81 | ? {}
82 | : {
83 | position: 'fixed',
84 | top: 0,
85 | left: 0,
86 | right: 0,
87 | zIndex: -1,
88 | }
89 | }
90 | },
91 | [components]
92 | )
93 |
94 | return (
95 | <>
96 | {isTransitioning && (
97 |
108 | )}
109 |
110 | {components.map((Page, i) => (
111 | (i === 0 ? (current.current = node) : (next.current = node))}
113 | style={getPosition(i)}
114 | {...Page.pageProps}
115 | key={Page.pageProps.id || Page.render?.displayName}
116 | />
117 | ))}
118 |
119 | >
120 | )
121 | }
122 |
--------------------------------------------------------------------------------
/website/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const sassUtils = require(__dirname + '/libs/sass-utils')
3 | const sassVars = require(__dirname + '/config/variables.js')
4 |
5 | const nextConfig = {
6 | reactStrictMode: true,
7 | transpilePackages: [],
8 | experimental: {
9 | optimizeCss: true,
10 | legacyBrowsers: false,
11 | // storyblok preview
12 | nextScriptWorkers: process.env.NODE_ENV !== 'development',
13 | urlImports: ['https://cdn.skypack.dev', 'https://unpkg.com'],
14 | },
15 | compiler: {
16 | removeConsole: process.env.NODE_ENV !== 'development',
17 | },
18 | images: {
19 | // ADD in case you need to import SVGs in next/image component
20 | // dangerouslyAllowSVG: true,
21 | // contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
22 | domains: ['assets.studiofreight.com'],
23 | formats: ['image/avif', 'image/webp'],
24 | },
25 | sassOptions: {
26 | // add @import 'styles/_functions'; to all scss files.
27 | includePaths: [path.join(__dirname, 'styles')],
28 | prependData: `@import 'styles/_functions';`,
29 | functions: {
30 | 'get($keys)': function (keys) {
31 | keys = keys.getValue().split('.')
32 | let result = sassVars
33 | for (let i = 0; i < keys.length; i++) {
34 | result = result[keys[i]]
35 | }
36 | result = sassUtils.castToSass(result)
37 |
38 | return result
39 | },
40 | 'getColors()': function () {
41 | return sassUtils.castToSass(sassVars.colors)
42 | },
43 | 'getThemes()': function () {
44 | return sassUtils.castToSass(sassVars.themes)
45 | },
46 | },
47 | },
48 | webpack: (config, options) => {
49 | const { dir } = options
50 |
51 | config.module.rules.push(
52 | {
53 | test: /\.svg$/,
54 | use: [
55 | {
56 | loader: '@svgr/webpack',
57 | options: {
58 | memo: true,
59 | dimensions: false,
60 | svgoConfig: {
61 | multipass: true,
62 | plugins: [
63 | 'removeDimensions',
64 | 'removeOffCanvasPaths',
65 | 'reusePaths',
66 | 'removeElementsByAttr',
67 | 'removeStyleElement',
68 | 'removeScriptElement',
69 | 'prefixIds',
70 | 'cleanupIds',
71 | {
72 | name: 'cleanupNumericValues',
73 | params: {
74 | floatPrecision: 1,
75 | },
76 | },
77 | {
78 | name: 'convertPathData',
79 | params: {
80 | floatPrecision: 1,
81 | },
82 | },
83 | {
84 | name: 'convertTransform',
85 | params: {
86 | floatPrecision: 1,
87 | },
88 | },
89 | {
90 | name: 'cleanupListOfValues',
91 | params: {
92 | floatPrecision: 1,
93 | },
94 | },
95 | ],
96 | },
97 | },
98 | },
99 | ],
100 | },
101 | {
102 | test: /\.(graphql|gql)$/,
103 | include: [dir],
104 | exclude: /node_modules/,
105 | use: [
106 | {
107 | loader: 'graphql-tag/loader',
108 | },
109 | ],
110 | },
111 | )
112 |
113 | return config
114 | },
115 | headers: async () => {
116 | return [
117 | {
118 | source: '/(.*)',
119 | headers: [
120 | {
121 | key: 'X-Content-Type-Options',
122 | value: 'nosniff',
123 | },
124 | {
125 | key: 'X-Frame-Options',
126 | value: 'SAMEORIGIN',
127 | },
128 | {
129 | key: 'X-XSS-Protection',
130 | value: '1; mode=block',
131 | },
132 | ],
133 | },
134 | ]
135 | },
136 | redirects: async () => {
137 | return [
138 | {
139 | source: '/home',
140 | destination: '/',
141 | permanent: true,
142 | },
143 | ]
144 | },
145 | }
146 |
147 | module.exports = () => {
148 | const plugins = []
149 | return plugins.reduce((acc, plugin) => plugin(acc), {
150 | ...nextConfig,
151 | })
152 | }
153 |
--------------------------------------------------------------------------------
/website/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
94 |
--------------------------------------------------------------------------------