mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-17 03:29:31 -07:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
758a2d1a62 | ||
|
|
ec1df7f070 | ||
|
|
f09988f32d | ||
|
|
e16aaca42b | ||
|
|
80442eb569 | ||
|
|
9039e04b38 | ||
|
|
56304e09f1 | ||
|
|
0cbaacbc0e | ||
|
|
f78449adb9 | ||
|
|
4d55bca248 | ||
|
|
ad998ba153 | ||
|
|
bd95ab861b | ||
|
|
919489c7e9 | ||
|
|
efa72bb763 | ||
|
|
90f79113c7 | ||
|
|
b5d0754c45 | ||
|
|
8a751b7437 | ||
|
|
212dca53e8 | ||
|
|
989a7f4951 | ||
|
|
f25b71a858 | ||
|
|
1c386386f3 | ||
|
|
797b59a38a | ||
|
|
dbbc063353 | ||
|
|
f7e87c3c15 | ||
|
|
1242146140 | ||
|
|
a8c1f948db | ||
|
|
dc30a8cd16 | ||
|
|
71c7b02c1d | ||
|
|
6cfcfb272a | ||
|
|
c239112cb2 | ||
|
|
637391bb34 | ||
|
|
fe38e1289a | ||
|
|
08bd9a76ef | ||
|
|
14f481b2c0 | ||
|
|
546791e90b | ||
|
|
136c2303f9 | ||
|
|
b13f1bf454 | ||
|
|
313a6f294a | ||
|
|
2804be174d | ||
|
|
fec976ad9e | ||
|
|
582433874c | ||
|
|
037cdbd679 | ||
|
|
50ff7f62bb | ||
|
|
3fe8477646 | ||
|
|
64b43597a4 | ||
|
|
94603999d7 | ||
|
|
c3c42e4aca | ||
|
|
38f4895233 | ||
|
|
636fea3496 | ||
|
|
c15b8a3e46 | ||
|
|
0cd891d16b | ||
|
|
4670c00157 | ||
|
|
032a75f581 | ||
|
|
5e40e9041e | ||
|
|
9c7731b8e5 | ||
|
|
d59475fbdc | ||
|
|
27c3b754ac | ||
|
|
1359adee59 | ||
|
|
b8d7193fb9 | ||
|
|
a06027401b | ||
|
|
5501d10cd0 | ||
|
|
31e20f752a | ||
|
|
fd300d8104 | ||
|
|
e91a7c6633 | ||
|
|
321e4b618a | ||
|
|
f630b85669 | ||
|
|
a431d74e8c | ||
|
|
975e084ea8 | ||
|
|
771b9b34ab | ||
|
|
1e5155c49e | ||
|
|
44750037f3 | ||
|
|
9af0090644 | ||
|
|
b05eec64cb | ||
|
|
284cfa4e84 | ||
|
|
11ea5c101b | ||
|
|
d4b9a4588a | ||
|
|
4ed332314d | ||
|
|
82ab5b996f | ||
|
|
81c14d9f2b | ||
|
|
7cf5f9ad2d | ||
|
|
0f69ec5f96 | ||
|
|
e6a9cacfef | ||
|
|
b24e401a28 | ||
|
|
ca9fef6c2d | ||
|
|
2835d46a57 | ||
|
|
3779bc7ce6 | ||
|
|
de16c6b443 | ||
|
|
d536303f8e | ||
|
|
76d420c2c9 | ||
|
|
c448648127 | ||
|
|
7e0e6244b0 | ||
|
|
c76835d474 | ||
|
|
39c13e242d | ||
|
|
61c86692ee | ||
|
|
cd63e9fec5 | ||
|
|
8bc8fded81 | ||
|
|
8bc54835e5 | ||
|
|
bd632b0ca4 | ||
|
|
b550dcd236 | ||
|
|
1b067c26d0 | ||
|
|
665522c8d1 | ||
|
|
e67865b82b | ||
|
|
6440556242 | ||
|
|
59227fb265 | ||
|
|
d291aab64f | ||
|
|
c290595a35 | ||
|
|
a6a576246f | ||
|
|
93629a4e6b | ||
|
|
a909afee97 | ||
|
|
9c15a367ea | ||
|
|
73ee16c6fb | ||
|
|
91b0314c5d | ||
|
|
d1a1dba210 | ||
|
|
110ff665e7 | ||
|
|
b39d563e77 | ||
|
|
0e74655820 | ||
|
|
8926caed69 | ||
|
|
19f54640f9 | ||
|
|
56b87d6393 | ||
|
|
75d8310953 | ||
|
|
a9f164f2af | ||
|
|
7773f184aa | ||
|
|
bc501934ae | ||
|
|
44b619e49b | ||
|
|
024d013219 | ||
|
|
268159e7d2 | ||
|
|
2e7131a5a5 | ||
|
|
1de3926c49 | ||
|
|
4b1f529589 | ||
|
|
8b3b5cd77a | ||
|
|
3a0025ebd1 | ||
|
|
283f386371 | ||
|
|
4c91cd269e | ||
|
|
304abf3b65 | ||
|
|
99a2866d4a |
97
.github/workflows/build.yml
vendored
97
.github/workflows/build.yml
vendored
@@ -12,93 +12,72 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
filter:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Filter
|
|
||||||
outputs:
|
|
||||||
any_changed: ${{ steps.changed-files-specific.outputs.any_changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get changed files in scope
|
|
||||||
id: changed-files-specific
|
|
||||||
uses: tj-actions/changed-files@v46
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
package.json
|
|
||||||
assets/**
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build (Hugo ${{ matrix.hugo-version }})
|
name: Build (Hugo ${{ matrix.hugo-label }})
|
||||||
needs: [filter]
|
if: github.event.repository.fork == false
|
||||||
if: |
|
|
||||||
github.ref == 'refs/heads/main' &&
|
|
||||||
github.event.repository.fork == false
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
hugo-version: ['latest', '0.114.0']
|
hugo-version: ['latest', '0.114.0']
|
||||||
|
include:
|
||||||
|
- hugo-version: latest
|
||||||
|
hugo-label: Latest
|
||||||
|
- hugo-version: '0.114.0'
|
||||||
|
hugo-label: 'v0.114.0'
|
||||||
steps:
|
steps:
|
||||||
- name: Set current date as env variable
|
|
||||||
run: |
|
|
||||||
echo "builddate=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PAT }}
|
token: ${{ secrets.PAT }}
|
||||||
|
|
||||||
- name: Setup Hugo
|
- name: Setup Mise
|
||||||
uses: peaceiris/actions-hugo@v2.6.0
|
uses: jdx/mise-action@v3
|
||||||
with:
|
with:
|
||||||
hugo-version: ${{ matrix.hugo-version }}
|
install_args: node@latest pnpm@10 hugo-extended@${{ matrix.hugo-version }}
|
||||||
extended: true
|
tool_versions: |
|
||||||
|
node latest
|
||||||
|
pnpm 10
|
||||||
|
hugo-extended ${{ matrix.hugo-version }}
|
||||||
|
cache: true
|
||||||
|
|
||||||
- name: Setup Dart Sass
|
- name: Get pnpm store path
|
||||||
run: sudo snap install dart-sass
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
id: pnpm-cache
|
||||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
run: 'echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT'
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
key: pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pnpm-store-
|
pnpm-store-
|
||||||
|
|
||||||
- name: Setup hugo cache
|
- name: Setup Hugo cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ./exampleSite/resources
|
path: exampleSite/resources/_gen
|
||||||
key: ${{ runner.os }}-hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite') }}
|
key: hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite/**/*.jpg') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-hugo-${{ matrix.hugo-version }}-
|
hugo-${{ matrix.hugo-version }}-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install project dependencies
|
||||||
run: pnpm install
|
run: 'pnpm install'
|
||||||
|
|
||||||
|
- name: Pre-build cleanup
|
||||||
|
if: >
|
||||||
|
matrix.hugo-version == 'latest' &&
|
||||||
|
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||||
|
run: 'rm -rf bundled'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: 'pnpm run build'
|
||||||
pnpm run vite:build
|
|
||||||
hugo --logLevel info --source=exampleSite --gc --minify
|
|
||||||
|
|
||||||
- name: Push artifacts
|
- name: Push artifacts
|
||||||
if: >
|
if: >
|
||||||
matrix.hugo-version == 'latest' &&
|
matrix.hugo-version == 'latest' &&
|
||||||
(github.event_name == 'push' || github.event.pull_request.merged == true) &&
|
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||||
needs.filter.outputs.any_changed == 'true'
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
|
||||||
with:
|
with:
|
||||||
|
file_pattern: 'bundled/**/*.js bundled/**/*.css'
|
||||||
commit_message: 'ci: update bundled artifacts [skip ci]'
|
commit_message: 'ci: update bundled artifacts [skip ci]'
|
||||||
|
|||||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -46,11 +46,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -77,6 +77,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
57
.github/workflows/eslint.yml
vendored
57
.github/workflows/eslint.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: 'ESLint && Prettier'
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Lint
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.PAT || github.token }}
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
|
||||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: ${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Lint Check
|
|
||||||
continue-on-error: true
|
|
||||||
id: check
|
|
||||||
run: pnpm run lint:check
|
|
||||||
|
|
||||||
- name: Format manually
|
|
||||||
id: format
|
|
||||||
if: ${{ steps.check.outcome == 'failure' }}
|
|
||||||
run: pnpm run lint
|
|
||||||
|
|
||||||
- name: Commit
|
|
||||||
if: ${{ steps.format.outcome == 'success' }}
|
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
|
||||||
with:
|
|
||||||
commit_message: 'ci: format code [skip ci]'
|
|
||||||
58
.github/workflows/lint.yml
vendored
Normal file
58
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
name: 'Lint'
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Lint
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
# github.token as a fallback, since other user might trigger this
|
||||||
|
# workflow in their pull request
|
||||||
|
token: ${{ secrets.PAT || github.token }}
|
||||||
|
|
||||||
|
- name: Setup Mise
|
||||||
|
uses: jdx/mise-action@v3
|
||||||
|
with:
|
||||||
|
install_args: node@latest pnpm@10
|
||||||
|
tool_versions: |
|
||||||
|
node latest
|
||||||
|
pnpm 10
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get pnpm store path
|
||||||
|
id: pnpm-cache
|
||||||
|
run: 'echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT'
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
|
key: pnpm-store-${{ hashFiles('./pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
pnpm-store-
|
||||||
|
|
||||||
|
- name: Install project dependencies
|
||||||
|
run: 'pnpm install'
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
id: lint
|
||||||
|
run: 'pnpm run lint:check || pnpm run lint'
|
||||||
|
|
||||||
|
- name: Commit
|
||||||
|
if: ${{ steps.format.lint == 'success' }}
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
|
with:
|
||||||
|
commit_message: 'ci: lint [skip ci]'
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Hugo default output directory
|
# Hugo default output directory
|
||||||
public/
|
public/
|
||||||
/exampleSite/resources/
|
exampleSite/resources/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
@@ -25,3 +25,6 @@ jsconfig.json
|
|||||||
|
|
||||||
# css map
|
# css map
|
||||||
*.css.map
|
*.css.map
|
||||||
|
|
||||||
|
# dummmy file
|
||||||
|
bundled/js/critical.js
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
node_modules
|
node_modules/
|
||||||
static
|
static/
|
||||||
exmapleSite
|
exmapleSite/
|
||||||
single.json
|
single.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
bundled/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
Bridget is a minimal [Hugo](https://gohugo.io) theme for photographers/visual artists, powered by [Solid.js](https://www.solidjs.com). Based on the https://github.com/tylermcrobert/bridget-pictures-www.
|
Bridget is a minimal [Hugo](https://gohugo.io) theme for photographers/visual artists, based on https://github.com/tylermcrobert/bridget-pictures-www.
|
||||||
|
|
||||||
Here is a [live demo](https://bridget-demo.sped0n.com).
|
Here is a [live demo](https://bridget-demo.sped0n.com).
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Geist';
|
font-family: 'Geist';
|
||||||
src:
|
src:
|
||||||
url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
url(/* @vite-ignore */'{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||||
format('woff2 supports variations'),
|
format('woff2 supports variations'),
|
||||||
url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}') format('woff2-variations');
|
url(/* @vite-ignore */'{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||||
|
format('woff2-variations');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'FW';
|
font-family: 'FW';
|
||||||
src: url('/lib/fonts/fw.woff2') format('woff2');
|
src: url(/* @vite-ignore */'{{- "lib/fonts/fw.woff2" | absURL -}}') format('woff2');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: var(--nav-height);
|
top: var(--nav-height);
|
||||||
z-index: var(--z-nav-gallery);
|
z-index: var(--z-nav-gallery);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -25,8 +26,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
max-height: calc(var(--window-height) - 2 * var(--nav-height));
|
||||||
height: 100%;
|
max-width: 100%;
|
||||||
|
width: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.navClose {
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: calc(var(--z-nav-gallery) + 1);
|
||||||
|
|
||||||
|
min-width: 25%;
|
||||||
|
height: calc(var(--nav-height) * 2.5);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
margin-right: calc(var(--space-standard) * -1);
|
||||||
|
padding-right: var(--space-standard);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
assets/ts/configState.tsx
Normal file
91
assets/ts/configState.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
type JSX
|
||||||
|
} from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import { getThresholdSessionIndex } from './utils'
|
||||||
|
|
||||||
|
export interface ThresholdRelated {
|
||||||
|
threshold: number
|
||||||
|
trailLength: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigState {
|
||||||
|
thresholdIndex: number
|
||||||
|
threshold: number
|
||||||
|
trailLength: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigStateContextType = readonly [
|
||||||
|
Accessor<ConfigState>,
|
||||||
|
{
|
||||||
|
readonly incThreshold: () => void
|
||||||
|
readonly decThreshold: () => void
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const thresholds: ThresholdRelated[] = [
|
||||||
|
{ threshold: 20, trailLength: 20 },
|
||||||
|
{ threshold: 40, trailLength: 10 },
|
||||||
|
{ threshold: 80, trailLength: 5 },
|
||||||
|
{ threshold: 140, trailLength: 5 },
|
||||||
|
{ threshold: 200, trailLength: 5 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const ConfigStateContext = createContext<ConfigStateContextType>()
|
||||||
|
|
||||||
|
function getSafeThresholdIndex(): number {
|
||||||
|
const index = getThresholdSessionIndex()
|
||||||
|
if (index < 0 || index >= thresholds.length) return 2
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||||
|
const [thresholdIndex, setThresholdIndex] = createSignal(getSafeThresholdIndex())
|
||||||
|
|
||||||
|
const state = createMemo<ConfigState>(() => {
|
||||||
|
const current = thresholds[thresholdIndex()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
thresholdIndex: thresholdIndex(),
|
||||||
|
threshold: current.threshold,
|
||||||
|
trailLength: current.trailLength
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateThreshold = (stride: number): void => {
|
||||||
|
const nextIndex = thresholdIndex() + stride
|
||||||
|
if (nextIndex < 0 || nextIndex >= thresholds.length) return
|
||||||
|
sessionStorage.setItem('thresholdsIndex', nextIndex.toString())
|
||||||
|
setThresholdIndex(nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigStateContext.Provider
|
||||||
|
value={[
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
incThreshold: () => {
|
||||||
|
updateThreshold(1)
|
||||||
|
},
|
||||||
|
decThreshold: () => {
|
||||||
|
updateThreshold(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ConfigStateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfigState(): ConfigStateContextType {
|
||||||
|
const context = useContext(ConfigStateContext)
|
||||||
|
invariant(context, 'undefined config context')
|
||||||
|
return context
|
||||||
|
}
|
||||||
2
assets/ts/critical.ts
Normal file
2
assets/ts/critical.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// this is a dummy file to trick vite to generate a critical.css file
|
||||||
|
import '../scss/critical.scss'
|
||||||
@@ -4,7 +4,6 @@ export default function CustomCursor(props: {
|
|||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
active: Accessor<boolean>
|
active: Accessor<boolean>
|
||||||
cursorText: Accessor<string>
|
cursorText: Accessor<string>
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// types
|
// types
|
||||||
interface XY {
|
interface XY {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Show, createMemo, createSignal, type JSX } from 'solid-js'
|
import { Show, createMemo, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import { useImageState } from '../imageState'
|
||||||
import type { Vector } from '../utils'
|
|
||||||
|
|
||||||
import CustomCursor from './customCursor'
|
import CustomCursor from './customCursor'
|
||||||
import Nav from './nav'
|
import Nav from './nav'
|
||||||
import Stage from './stage'
|
import Stage from './stage'
|
||||||
import StageNav from './stageNav'
|
import StageNav from './stageNav'
|
||||||
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interfaces and types
|
* interfaces and types
|
||||||
@@ -23,65 +23,36 @@ export interface DesktopImage extends HTMLImageElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryItem {
|
|
||||||
i: number
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* components
|
* components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function Desktop(props: {
|
export default function Desktop(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
ijs: ImageJSON[]
|
|
||||||
prevText: string
|
prevText: string
|
||||||
closeText: string
|
closeText: string
|
||||||
nextText: string
|
nextText: string
|
||||||
loadingText: string
|
loadingText: string
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
const imageState = useImageState()
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [desktop] = useDesktopState()
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
|
||||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
|
||||||
const [hoverText, setHoverText] = createSignal('')
|
|
||||||
const [navVector, setNavVector] = createSignal<Vector>('none')
|
|
||||||
|
|
||||||
const active = createMemo(() => isOpen() && !isAnimating())
|
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||||
const cursorText = createMemo(() => (isLoading() ? props.loadingText : hoverText()))
|
const cursorText = createMemo(() =>
|
||||||
|
desktop.isLoading() ? props.loadingText : desktop.hoverText()
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<Show when={props.ijs.length > 0}>
|
<Show when={imageState().length > 0}>
|
||||||
<Stage
|
<Stage />
|
||||||
ijs={props.ijs}
|
<Show when={desktop.isOpen()}>
|
||||||
setIsLoading={setIsLoading}
|
<CustomCursor cursorText={cursorText} active={active} />
|
||||||
isOpen={isOpen}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
isAnimating={isAnimating}
|
|
||||||
setIsAnimating={setIsAnimating}
|
|
||||||
cordHist={cordHist}
|
|
||||||
setCordHist={setCordHist}
|
|
||||||
navVector={navVector}
|
|
||||||
setNavVector={setNavVector}
|
|
||||||
/>
|
|
||||||
<Show when={isOpen()}>
|
|
||||||
<CustomCursor cursorText={cursorText} active={active} isOpen={isOpen} />
|
|
||||||
<StageNav
|
<StageNav
|
||||||
prevText={props.prevText}
|
prevText={props.prevText}
|
||||||
closeText={props.closeText}
|
closeText={props.closeText}
|
||||||
nextText={props.nextText}
|
nextText={props.nextText}
|
||||||
loadingText={props.loadingText}
|
|
||||||
active={active}
|
|
||||||
isAnimating={isAnimating}
|
|
||||||
setCordHist={setCordHist}
|
|
||||||
isOpen={isOpen}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
setHoverText={setHoverText}
|
|
||||||
navVector={navVector}
|
|
||||||
setNavVector={setNavVector}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,66 +1,68 @@
|
|||||||
import { createEffect } from 'solid-js'
|
import { createEffect, onCleanup, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useState } from '../state'
|
import { useConfigState } from '../configState'
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
import { expand } from '../utils'
|
import { expand } from '../utils'
|
||||||
|
|
||||||
/**
|
import { useDesktopState } from './state'
|
||||||
* constants
|
|
||||||
*/
|
|
||||||
|
|
||||||
// threshold div
|
|
||||||
const thresholdDiv = document.getElementsByClassName('threshold')[0] as HTMLDivElement
|
|
||||||
// threshold nums span
|
|
||||||
const thresholdDispNums = Array.from(
|
|
||||||
thresholdDiv.getElementsByClassName('num')
|
|
||||||
) as HTMLSpanElement[]
|
|
||||||
// threshold buttons
|
|
||||||
const decButton = thresholdDiv
|
|
||||||
.getElementsByClassName('dec')
|
|
||||||
.item(0) as HTMLButtonElement
|
|
||||||
const incButton = thresholdDiv
|
|
||||||
.getElementsByClassName('inc')
|
|
||||||
.item(0) as HTMLButtonElement
|
|
||||||
// index div
|
|
||||||
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
|
||||||
// index nums span
|
|
||||||
const indexDispNums = Array.from(
|
|
||||||
indexDiv.getElementsByClassName('num')
|
|
||||||
) as HTMLSpanElement[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function updateThresholdText(thresholdValue: string): void {
|
|
||||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
|
||||||
e.innerText = thresholdValue[i]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIndexText(indexValue: string, indexLength: string): void {
|
|
||||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
|
||||||
if (i < 4) {
|
|
||||||
e.innerText = indexValue[i]
|
|
||||||
} else {
|
|
||||||
e.innerText = indexLength[i - 4]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nav component
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Nav(): null {
|
export default function Nav(): null {
|
||||||
const [state, { incThreshold, decThreshold }] = useState()
|
let thresholdNums: HTMLSpanElement[] = []
|
||||||
|
let indexNums: HTMLSpanElement[] = []
|
||||||
|
let decButton: HTMLButtonElement | undefined
|
||||||
|
let incButton: HTMLButtonElement | undefined
|
||||||
|
let controller: AbortController | undefined
|
||||||
|
|
||||||
createEffect(() => {
|
const imageState = useImageState()
|
||||||
updateIndexText(expand(state().index + 1), expand(state().length))
|
const [config, { incThreshold, decThreshold }] = useConfigState()
|
||||||
updateThresholdText(expand(state().threshold))
|
const [desktop] = useDesktopState()
|
||||||
|
|
||||||
|
const updateThresholdText = (thresholdValue: string): void => {
|
||||||
|
thresholdNums.forEach((element, i) => {
|
||||||
|
element.innerText = thresholdValue[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateIndexText = (indexValue: string, indexLength: string): void => {
|
||||||
|
indexNums.forEach((element, i) => {
|
||||||
|
if (i < 4) {
|
||||||
|
element.innerText = indexValue[i]
|
||||||
|
} else {
|
||||||
|
element.innerText = indexLength[i - 4]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const thresholdDiv = document.getElementsByClassName(
|
||||||
|
'threshold'
|
||||||
|
)[0] as HTMLDivElement
|
||||||
|
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
||||||
|
|
||||||
|
thresholdNums = Array.from(
|
||||||
|
thresholdDiv.getElementsByClassName('num')
|
||||||
|
) as HTMLSpanElement[]
|
||||||
|
indexNums = Array.from(indexDiv.getElementsByClassName('num')) as HTMLSpanElement[]
|
||||||
|
decButton = thresholdDiv.getElementsByClassName('dec').item(0) as HTMLButtonElement
|
||||||
|
incButton = thresholdDiv.getElementsByClassName('inc').item(0) as HTMLButtonElement
|
||||||
|
|
||||||
|
controller = new AbortController()
|
||||||
|
const signal = controller.signal
|
||||||
|
|
||||||
|
decButton.addEventListener('click', decThreshold, { signal })
|
||||||
|
incButton.addEventListener('click', incThreshold, { signal })
|
||||||
})
|
})
|
||||||
|
|
||||||
decButton.onclick = decThreshold
|
createEffect(() => {
|
||||||
incButton.onclick = incThreshold
|
if (thresholdNums.length === 0 || indexNums.length === 0) return
|
||||||
|
|
||||||
|
updateIndexText(expand(desktop.index() + 1), expand(imageState().length))
|
||||||
|
updateThresholdText(expand(config().threshold))
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
controller?.abort()
|
||||||
|
})
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,409 +1,167 @@
|
|||||||
import { type gsap } from 'gsap'
|
import { type gsap } from 'gsap'
|
||||||
import {
|
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
For,
|
|
||||||
createEffect,
|
|
||||||
on,
|
|
||||||
onMount,
|
|
||||||
type Accessor,
|
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import { useConfigState } from '../configState'
|
||||||
import { useState, type State } from '../state'
|
import { useImageState } from '../imageState'
|
||||||
import { decrement, increment, loadGsap, type Vector } from '../utils'
|
import { increment, loadGsap } from '../utils'
|
||||||
|
|
||||||
import type { DesktopImage, HistoryItem } from './layout'
|
import type { DesktopImage } from './layout'
|
||||||
|
import { expandStage, minimizeStage, syncStagePosition } from './stageAnimations'
|
||||||
|
import { onMutation } from './stageUtils'
|
||||||
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
/**
|
export default function Stage(): JSX.Element {
|
||||||
* helper functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
|
|
||||||
return cordHistValue.map((el) => el.i)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrailCurrentElsIndex(
|
|
||||||
cordHistValue: HistoryItem[],
|
|
||||||
stateValue: State
|
|
||||||
): number[] {
|
|
||||||
return getTrailElsIndex(cordHistValue).slice(-stateValue.trailLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrailInactiveElsIndex(
|
|
||||||
cordHistValue: HistoryItem[],
|
|
||||||
stateValue: State
|
|
||||||
): number[] {
|
|
||||||
return getTrailCurrentElsIndex(cordHistValue, stateValue).slice(0, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
|
|
||||||
return getTrailElsIndex(cordHistValue).slice(-1)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrevElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
|
|
||||||
return decrement(cordHistValue.slice(-1)[0].i, stateValue.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextElIndex(cordHistValue: HistoryItem[], stateValue: State): number {
|
|
||||||
return increment(cordHistValue.slice(-1)[0].i, stateValue.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImagesFromIndexes(imgs: DesktopImage[], indexes: number[]): DesktopImage[] {
|
|
||||||
return indexes.map((i) => imgs[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
function hires(imgs: DesktopImage[]): void {
|
|
||||||
imgs.forEach((img) => {
|
|
||||||
if (img.src === img.dataset.hiUrl) return
|
|
||||||
img.src = img.dataset.hiUrl
|
|
||||||
img.height = parseInt(img.dataset.hiImgH)
|
|
||||||
img.width = parseInt(img.dataset.hiImgW)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function lores(imgs: DesktopImage[]): void {
|
|
||||||
imgs.forEach((img) => {
|
|
||||||
if (img.src === img.dataset.loUrl) return
|
|
||||||
img.src = img.dataset.loUrl
|
|
||||||
img.height = parseInt(img.dataset.loImgH)
|
|
||||||
img.width = parseInt(img.dataset.loImgW)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMutation<T extends HTMLElement>(
|
|
||||||
element: T,
|
|
||||||
trigger: (arg0: MutationRecord) => boolean,
|
|
||||||
observeOptions: MutationObserverInit = { attributes: true }
|
|
||||||
): void {
|
|
||||||
new MutationObserver((mutations, observer) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (trigger(mutation)) {
|
|
||||||
observer.disconnect()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).observe(element, observeOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stage component
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Stage(props: {
|
|
||||||
ijs: ImageJSON[]
|
|
||||||
setIsLoading: Setter<boolean>
|
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
setIsAnimating: Setter<boolean>
|
|
||||||
cordHist: Accessor<HistoryItem[]>
|
|
||||||
setCordHist: Setter<HistoryItem[]>
|
|
||||||
navVector: Accessor<Vector>
|
|
||||||
setNavVector: Setter<Vector>
|
|
||||||
}): JSX.Element {
|
|
||||||
// variables
|
|
||||||
let _gsap: typeof gsap
|
let _gsap: typeof gsap
|
||||||
|
let gsapPromise: Promise<void> | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
const imageState = useImageState()
|
||||||
const imgs: DesktopImage[] = Array<DesktopImage>(props.ijs.length)
|
const [config] = useConfigState()
|
||||||
|
const [
|
||||||
|
desktop,
|
||||||
|
{ setIndex, setCordHist, setIsOpen, setIsAnimating, setIsLoading, setNavVector }
|
||||||
|
] = useDesktopState()
|
||||||
|
|
||||||
|
const imgs: DesktopImage[] = Array<DesktopImage>(imageState().length)
|
||||||
let last = { x: 0, y: 0 }
|
let last = { x: 0, y: 0 }
|
||||||
|
|
||||||
let abortController: AbortController | undefined
|
let abortController: AbortController | undefined
|
||||||
|
|
||||||
// states
|
|
||||||
let gsapLoaded = false
|
let gsapLoaded = false
|
||||||
|
|
||||||
const [state, { incIndex }] = useState()
|
|
||||||
const stateLength = state().length
|
|
||||||
|
|
||||||
let mounted = false
|
let mounted = false
|
||||||
|
|
||||||
|
const ensureGsapReady: () => Promise<void> = async () => {
|
||||||
|
if (gsapPromise !== undefined) return await gsapPromise
|
||||||
|
|
||||||
|
gsapPromise = loadGsap()
|
||||||
|
.then((g) => {
|
||||||
|
_gsap = g
|
||||||
|
gsapLoaded = true
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
gsapPromise = undefined
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
await gsapPromise
|
||||||
|
}
|
||||||
|
|
||||||
const onMouse: (e: MouseEvent) => void = (e) => {
|
const onMouse: (e: MouseEvent) => void = (e) => {
|
||||||
if (props.isOpen() || props.isAnimating() || !gsapLoaded || !mounted) return
|
if (desktop.isOpen() || desktop.isAnimating() || !gsapLoaded || !mounted) return
|
||||||
|
|
||||||
|
const length = imageState().length
|
||||||
|
if (length <= 0) return
|
||||||
|
|
||||||
const cord = { x: e.clientX, y: e.clientY }
|
const cord = { x: e.clientX, y: e.clientY }
|
||||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||||
|
|
||||||
if (travelDist > state().threshold) {
|
if (travelDist > config().threshold) {
|
||||||
last = cord
|
const nextIndex = increment(desktop.index(), length)
|
||||||
incIndex()
|
|
||||||
|
|
||||||
const _state = state()
|
last = cord
|
||||||
const newHist = { i: _state.index, ...cord }
|
setIndex(nextIndex)
|
||||||
props.setCordHist((prev) => [...prev, newHist].slice(-stateLength))
|
setCordHist((prev) => [...prev, { i: nextIndex, ...cord }].slice(-length))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick: () => void = () => {
|
const onClick: () => Promise<void> = async () => {
|
||||||
if (!props.isAnimating()) props.setIsOpen(true)
|
if (!gsapLoaded) {
|
||||||
|
await ensureGsapReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktop.isAnimating() || !gsapLoaded) return
|
||||||
|
if (desktop.index() < 0 || desktop.cordHist().length === 0) return
|
||||||
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPosition: () => void = () => {
|
const setPosition: () => void = () => {
|
||||||
if (!mounted) return
|
syncStagePosition({
|
||||||
if (imgs.length === 0) return
|
gsap: _gsap,
|
||||||
const _cordHist = props.cordHist()
|
|
||||||
const trailElsIndex = getTrailElsIndex(_cordHist)
|
|
||||||
if (trailElsIndex.length === 0) return
|
|
||||||
|
|
||||||
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
|
|
||||||
|
|
||||||
const _isOpen = props.isOpen()
|
|
||||||
const _state = state()
|
|
||||||
|
|
||||||
_gsap.set(elsTrail, {
|
|
||||||
x: (i: number) => _cordHist[i].x - window.innerWidth / 2,
|
|
||||||
y: (i: number) => _cordHist[i].y - window.innerHeight / 2,
|
|
||||||
opacity: (i: number) =>
|
|
||||||
Math.max(
|
|
||||||
(i + 1 + _state.trailLength <= _cordHist.length ? 0 : 1) - (_isOpen ? 1 : 0),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
zIndex: (i: number) => i,
|
|
||||||
scale: 0.6
|
|
||||||
})
|
|
||||||
|
|
||||||
if (_isOpen) {
|
|
||||||
const elc = getImagesFromIndexes(imgs, [getCurrentElIndex(_cordHist)])[0]
|
|
||||||
const indexArrayToHires: number[] = []
|
|
||||||
const indexArrayToCleanup: number[] = []
|
|
||||||
switch (props.navVector()) {
|
|
||||||
case 'prev':
|
|
||||||
indexArrayToHires.push(getPrevElIndex(_cordHist, _state))
|
|
||||||
indexArrayToCleanup.push(getNextElIndex(_cordHist, _state))
|
|
||||||
break
|
|
||||||
case 'next':
|
|
||||||
indexArrayToHires.push(getNextElIndex(_cordHist, _state))
|
|
||||||
indexArrayToCleanup.push(getPrevElIndex(_cordHist, _state))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
hires(getImagesFromIndexes(imgs, indexArrayToHires)) // preload
|
|
||||||
_gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
|
|
||||||
_gsap.set(elc, { x: 0, y: 0, scale: 1 }) // set current to center
|
|
||||||
setLoaderForHiresImage(elc) // set loader, if loaded set current opacity to 1
|
|
||||||
} else {
|
|
||||||
lores(elsTrail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandImage: () => Promise<
|
|
||||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
|
||||||
> = async () => {
|
|
||||||
// isAnimating is prechecked in isOpen effect
|
|
||||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
|
||||||
|
|
||||||
props.setIsAnimating(true)
|
|
||||||
|
|
||||||
const _cordHist = props.cordHist()
|
|
||||||
const _state = state()
|
|
||||||
|
|
||||||
const elcIndex = getCurrentElIndex(_cordHist)
|
|
||||||
const elc = imgs[elcIndex]
|
|
||||||
|
|
||||||
// don't hide here because we want a better transition
|
|
||||||
hires(
|
|
||||||
getImagesFromIndexes(imgs, [
|
|
||||||
elcIndex,
|
|
||||||
getPrevElIndex(_cordHist, _state),
|
|
||||||
getNextElIndex(_cordHist, _state)
|
|
||||||
])
|
|
||||||
)
|
|
||||||
setLoaderForHiresImage(elc)
|
|
||||||
|
|
||||||
const tl = _gsap.timeline()
|
|
||||||
const trailInactiveEls = getImagesFromIndexes(
|
|
||||||
imgs,
|
imgs,
|
||||||
getTrailInactiveElsIndex(_cordHist, _state)
|
cordHist: desktop.cordHist(),
|
||||||
)
|
trailLength: config().trailLength,
|
||||||
// move down and hide trail inactive
|
length: imageState().length,
|
||||||
tl.to(trailInactiveEls, {
|
isOpen: desktop.isOpen(),
|
||||||
y: '+=20',
|
navVector: desktop.navVector(),
|
||||||
ease: 'power3.in',
|
mounted,
|
||||||
stagger: 0.075,
|
setIsLoading
|
||||||
duration: 0.3,
|
|
||||||
delay: 0.1,
|
|
||||||
opacity: 0
|
|
||||||
})
|
|
||||||
// current move to center
|
|
||||||
tl.to(elc, {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
ease: 'power3.inOut',
|
|
||||||
duration: 0.7,
|
|
||||||
delay: 0.3
|
|
||||||
})
|
|
||||||
// current expand
|
|
||||||
tl.to(elc, {
|
|
||||||
delay: 0.1,
|
|
||||||
scale: 1,
|
|
||||||
ease: 'power3.inOut'
|
|
||||||
})
|
|
||||||
// finished
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
return await tl.then(() => {
|
|
||||||
props.setIsAnimating(false)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const minimizeImage: () => Promise<
|
const expandImage: () => Promise<void> = async () => {
|
||||||
gsap.core.Omit<gsap.core.Timeline, 'then'>
|
|
||||||
> = async () => {
|
|
||||||
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||||
|
|
||||||
props.setIsAnimating(true)
|
await expandStage({
|
||||||
props.setNavVector('none') // cleanup
|
gsap: _gsap,
|
||||||
|
imgs,
|
||||||
const _cordHist = props.cordHist()
|
cordHist: desktop.cordHist(),
|
||||||
const _state = state()
|
trailLength: config().trailLength,
|
||||||
|
length: imageState().length,
|
||||||
const elcIndex = getCurrentElIndex(_cordHist)
|
mounted,
|
||||||
const elsTrailInactiveIndexes = getTrailInactiveElsIndex(_cordHist, _state)
|
setIsLoading,
|
||||||
|
setIsAnimating
|
||||||
lores(getImagesFromIndexes(imgs, [...elsTrailInactiveIndexes, elcIndex]))
|
|
||||||
|
|
||||||
const tl = _gsap.timeline()
|
|
||||||
const elc = getImagesFromIndexes(imgs, [elcIndex])[0]
|
|
||||||
const elsTrailInactive = getImagesFromIndexes(imgs, elsTrailInactiveIndexes)
|
|
||||||
// shrink current
|
|
||||||
tl.to(elc, {
|
|
||||||
scale: 0.6,
|
|
||||||
duration: 0.6,
|
|
||||||
ease: 'power3.inOut'
|
|
||||||
})
|
|
||||||
// move current to original position
|
|
||||||
tl.to(elc, {
|
|
||||||
delay: 0.3,
|
|
||||||
duration: 0.7,
|
|
||||||
ease: 'power3.inOut',
|
|
||||||
x: _cordHist.slice(-1)[0].x - window.innerWidth / 2,
|
|
||||||
y: _cordHist.slice(-1)[0].y - window.innerHeight / 2
|
|
||||||
})
|
|
||||||
// show trail inactive
|
|
||||||
tl.to(elsTrailInactive, {
|
|
||||||
y: '-=20',
|
|
||||||
ease: 'power3.out',
|
|
||||||
stagger: -0.1,
|
|
||||||
duration: 0.3,
|
|
||||||
opacity: 1
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
return await tl.then(() => {
|
|
||||||
props.setIsAnimating(false)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLoaderForHiresImage(img: DesktopImage): void {
|
const minimizeImage: () => Promise<void> = async () => {
|
||||||
if (!mounted || !gsapLoaded) return
|
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||||
if (!img.complete) {
|
|
||||||
props.setIsLoading(true)
|
setNavVector('none')
|
||||||
// abort controller for cleanup
|
|
||||||
const controller = new AbortController()
|
await minimizeStage({
|
||||||
const abortSignal = controller.signal
|
gsap: _gsap,
|
||||||
// event listeners
|
imgs,
|
||||||
img.addEventListener(
|
cordHist: desktop.cordHist(),
|
||||||
'load',
|
trailLength: config().trailLength,
|
||||||
() => {
|
mounted,
|
||||||
_gsap
|
setIsAnimating
|
||||||
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
|
})
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
.then(() => {
|
|
||||||
props.setIsLoading(false)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
controller.abort()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ once: true, passive: true, signal: abortSignal }
|
|
||||||
)
|
|
||||||
img.addEventListener(
|
|
||||||
'error',
|
|
||||||
() => {
|
|
||||||
_gsap
|
|
||||||
.set(img, { opacity: 1 })
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
.then(() => {
|
|
||||||
props.setIsLoading(false)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
controller.abort()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ once: true, passive: true, signal: abortSignal }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
_gsap
|
|
||||||
.set(img, { opacity: 1 })
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
.then(() => {
|
|
||||||
props.setIsLoading(false)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// preload logic
|
|
||||||
imgs.forEach((img, i) => {
|
imgs.forEach((img, i) => {
|
||||||
// preload first 5 images on page load
|
|
||||||
if (i < 5) {
|
if (i < 5) {
|
||||||
img.src = img.dataset.loUrl
|
img.src = img.dataset.loUrl
|
||||||
}
|
}
|
||||||
// lores preloader for rest of the images
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
onMutation(img, (mutation) => {
|
onMutation(img, (mutation) => {
|
||||||
// if open or animating, hold
|
if (desktop.isOpen() || desktop.isAnimating()) return false
|
||||||
if (props.isOpen() || props.isAnimating()) return false
|
|
||||||
// if mutation is not about style attribute, hold
|
|
||||||
if (mutation.attributeName !== 'style') return false
|
if (mutation.attributeName !== 'style') return false
|
||||||
|
|
||||||
const opacity = parseFloat(img.style.opacity)
|
const opacity = parseFloat(img.style.opacity)
|
||||||
// if opacity is not 1, hold
|
|
||||||
if (opacity !== 1) return false
|
if (opacity !== 1) return false
|
||||||
// preload the i + 5th image, if it exists
|
|
||||||
if (i + 5 < imgs.length) {
|
if (i + 5 < imgs.length) {
|
||||||
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
||||||
}
|
}
|
||||||
// triggered
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// load gsap on mousemove
|
|
||||||
window.addEventListener(
|
window.addEventListener('pointermove', () => void ensureGsapReady(), {
|
||||||
'mousemove',
|
passive: true,
|
||||||
() => {
|
once: true
|
||||||
loadGsap()
|
})
|
||||||
.then((g) => {
|
window.addEventListener('pointerdown', () => void ensureGsapReady(), {
|
||||||
_gsap = g
|
passive: true,
|
||||||
gsapLoaded = true
|
once: true
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
window.addEventListener('click', () => void ensureGsapReady(), {
|
||||||
console.log(e)
|
passive: true,
|
||||||
})
|
once: true
|
||||||
},
|
})
|
||||||
{ passive: true, once: true }
|
|
||||||
)
|
|
||||||
// event listeners
|
|
||||||
abortController = new AbortController()
|
abortController = new AbortController()
|
||||||
const abortSignal = abortController.signal
|
const abortSignal = abortController.signal
|
||||||
window.addEventListener('mousemove', onMouse, {
|
window.addEventListener('mousemove', onMouse, {
|
||||||
passive: true,
|
passive: true,
|
||||||
signal: abortSignal
|
signal: abortSignal
|
||||||
})
|
})
|
||||||
// mounted
|
|
||||||
mounted = true
|
mounted = true
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.cordHist(),
|
() => desktop.cordHist(),
|
||||||
() => {
|
() => {
|
||||||
setPosition()
|
setPosition()
|
||||||
},
|
},
|
||||||
@@ -413,36 +171,38 @@ export default function Stage(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.isOpen(),
|
desktop.isOpen,
|
||||||
async () => {
|
async (isOpen) => {
|
||||||
if (props.isAnimating()) return
|
if (desktop.isAnimating()) return
|
||||||
if (props.isOpen()) {
|
|
||||||
// expand image
|
if (isOpen) {
|
||||||
|
if (desktop.index() < 0 || desktop.cordHist().length === 0) {
|
||||||
|
setIsOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await expandImage()
|
await expandImage()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
void 0
|
setIsOpen(false)
|
||||||
|
setIsAnimating(false)
|
||||||
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// abort controller for cleanup
|
|
||||||
abortController?.abort()
|
abortController?.abort()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// minimize image
|
|
||||||
await minimizeImage()
|
await minimizeImage()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
void 0
|
void 0
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// event listeners and its abort controller
|
|
||||||
abortController = new AbortController()
|
abortController = new AbortController()
|
||||||
const abortSignal = abortController.signal
|
const abortSignal = abortController.signal
|
||||||
window.addEventListener('mousemove', onMouse, {
|
window.addEventListener('mousemove', onMouse, {
|
||||||
passive: true,
|
passive: true,
|
||||||
signal: abortSignal
|
signal: abortSignal
|
||||||
})
|
})
|
||||||
// cleanup isLoading
|
setIsLoading(false)
|
||||||
props.setIsLoading(false)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -453,7 +213,7 @@ export default function Stage(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
||||||
<For each={props.ijs}>
|
<For each={imageState().images}>
|
||||||
{(ij, i) => (
|
{(ij, i) => (
|
||||||
<img
|
<img
|
||||||
ref={imgs[i()]}
|
ref={imgs[i()]}
|
||||||
|
|||||||
263
assets/ts/desktop/stageAnimations.ts
Normal file
263
assets/ts/desktop/stageAnimations.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { type gsap } from 'gsap'
|
||||||
|
|
||||||
|
import type { Vector } from '../utils'
|
||||||
|
|
||||||
|
import type { DesktopImage } from './layout'
|
||||||
|
import {
|
||||||
|
getCurrentElIndex,
|
||||||
|
getImagesFromIndexes,
|
||||||
|
getNextElIndex,
|
||||||
|
getPrevElIndex,
|
||||||
|
getTrailElsIndex,
|
||||||
|
getTrailInactiveElsIndex,
|
||||||
|
hires,
|
||||||
|
lores
|
||||||
|
} from './stageUtils'
|
||||||
|
import type { HistoryItem } from './state'
|
||||||
|
|
||||||
|
type SetLoading = (value: boolean) => void
|
||||||
|
|
||||||
|
export function setLoaderForHiresImage(args: {
|
||||||
|
gsap: typeof gsap
|
||||||
|
img: DesktopImage
|
||||||
|
mounted: boolean
|
||||||
|
setIsLoading: SetLoading
|
||||||
|
}): void {
|
||||||
|
const { gsap, img, mounted, setIsLoading } = args
|
||||||
|
if (!mounted) return
|
||||||
|
|
||||||
|
if (img.complete) {
|
||||||
|
gsap
|
||||||
|
.set(img, { opacity: 1 })
|
||||||
|
.then(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const abortSignal = controller.signal
|
||||||
|
|
||||||
|
img.addEventListener(
|
||||||
|
'load',
|
||||||
|
() => {
|
||||||
|
gsap
|
||||||
|
.to(img, { opacity: 1, ease: 'power3.out', duration: 0.5 })
|
||||||
|
.then(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
controller.abort()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ once: true, passive: true, signal: abortSignal }
|
||||||
|
)
|
||||||
|
img.addEventListener(
|
||||||
|
'error',
|
||||||
|
() => {
|
||||||
|
gsap
|
||||||
|
.set(img, { opacity: 1 })
|
||||||
|
.then(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
controller.abort()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ once: true, passive: true, signal: abortSignal }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncStagePosition(args: {
|
||||||
|
gsap: typeof gsap
|
||||||
|
imgs: DesktopImage[]
|
||||||
|
cordHist: HistoryItem[]
|
||||||
|
trailLength: number
|
||||||
|
length: number
|
||||||
|
isOpen: boolean
|
||||||
|
navVector: Vector
|
||||||
|
mounted: boolean
|
||||||
|
setIsLoading: SetLoading
|
||||||
|
}): void {
|
||||||
|
const {
|
||||||
|
gsap,
|
||||||
|
imgs,
|
||||||
|
cordHist,
|
||||||
|
trailLength,
|
||||||
|
length,
|
||||||
|
isOpen,
|
||||||
|
navVector,
|
||||||
|
mounted,
|
||||||
|
setIsLoading
|
||||||
|
} = args
|
||||||
|
|
||||||
|
if (!mounted || imgs.length === 0) return
|
||||||
|
|
||||||
|
const trailElsIndex = getTrailElsIndex(cordHist)
|
||||||
|
if (trailElsIndex.length === 0) return
|
||||||
|
|
||||||
|
const elsTrail = getImagesFromIndexes(imgs, trailElsIndex)
|
||||||
|
|
||||||
|
gsap.set(elsTrail, {
|
||||||
|
x: (i: number) => cordHist[i].x - window.innerWidth / 2,
|
||||||
|
y: (i: number) => cordHist[i].y - window.innerHeight / 2,
|
||||||
|
opacity: (i: number) =>
|
||||||
|
Math.max((i + 1 + trailLength <= cordHist.length ? 0 : 1) - (isOpen ? 1 : 0), 0),
|
||||||
|
zIndex: (i: number) => i,
|
||||||
|
scale: 0.6
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
lores(elsTrail)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = getImagesFromIndexes(imgs, [getCurrentElIndex(cordHist)])[0]
|
||||||
|
const indexArrayToHires: number[] = []
|
||||||
|
const indexArrayToCleanup: number[] = []
|
||||||
|
|
||||||
|
switch (navVector) {
|
||||||
|
case 'prev':
|
||||||
|
indexArrayToHires.push(getPrevElIndex(cordHist, length))
|
||||||
|
indexArrayToCleanup.push(getNextElIndex(cordHist, length))
|
||||||
|
break
|
||||||
|
case 'next':
|
||||||
|
indexArrayToHires.push(getNextElIndex(cordHist, length))
|
||||||
|
indexArrayToCleanup.push(getPrevElIndex(cordHist, length))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
hires(getImagesFromIndexes(imgs, indexArrayToHires))
|
||||||
|
gsap.set(getImagesFromIndexes(imgs, indexArrayToCleanup), { opacity: 0 })
|
||||||
|
gsap.set(current, { x: 0, y: 0, scale: 1 })
|
||||||
|
setLoaderForHiresImage({ gsap, img: current, mounted, setIsLoading })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expandStage(args: {
|
||||||
|
gsap: typeof gsap
|
||||||
|
imgs: DesktopImage[]
|
||||||
|
cordHist: HistoryItem[]
|
||||||
|
trailLength: number
|
||||||
|
length: number
|
||||||
|
mounted: boolean
|
||||||
|
setIsLoading: SetLoading
|
||||||
|
setIsAnimating: (value: boolean) => void
|
||||||
|
}): Promise<void> {
|
||||||
|
const {
|
||||||
|
gsap,
|
||||||
|
imgs,
|
||||||
|
cordHist,
|
||||||
|
trailLength,
|
||||||
|
length,
|
||||||
|
mounted,
|
||||||
|
setIsLoading,
|
||||||
|
setIsAnimating
|
||||||
|
} = args
|
||||||
|
|
||||||
|
if (!mounted) throw new Error('not mounted')
|
||||||
|
|
||||||
|
setIsAnimating(true)
|
||||||
|
|
||||||
|
const currentIndex = getCurrentElIndex(cordHist)
|
||||||
|
const current = imgs[currentIndex]
|
||||||
|
|
||||||
|
hires(
|
||||||
|
getImagesFromIndexes(imgs, [
|
||||||
|
currentIndex,
|
||||||
|
getPrevElIndex(cordHist, length),
|
||||||
|
getNextElIndex(cordHist, length)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
setLoaderForHiresImage({ gsap, img: current, mounted, setIsLoading })
|
||||||
|
|
||||||
|
const tl = gsap.timeline()
|
||||||
|
const trailInactiveEls = getImagesFromIndexes(
|
||||||
|
imgs,
|
||||||
|
getTrailInactiveElsIndex(cordHist, trailLength)
|
||||||
|
)
|
||||||
|
|
||||||
|
tl.to(trailInactiveEls, {
|
||||||
|
y: '+=20',
|
||||||
|
ease: 'power3.in',
|
||||||
|
stagger: 0.075,
|
||||||
|
duration: 0.3,
|
||||||
|
delay: 0.1,
|
||||||
|
opacity: 0
|
||||||
|
})
|
||||||
|
tl.to(current, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
ease: 'power3.inOut',
|
||||||
|
duration: 0.7,
|
||||||
|
delay: 0.3
|
||||||
|
})
|
||||||
|
tl.to(current, {
|
||||||
|
delay: 0.1,
|
||||||
|
scale: 1,
|
||||||
|
ease: 'power3.inOut'
|
||||||
|
})
|
||||||
|
|
||||||
|
await tl.then(() => {
|
||||||
|
setIsAnimating(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function minimizeStage(args: {
|
||||||
|
gsap: typeof gsap
|
||||||
|
imgs: DesktopImage[]
|
||||||
|
cordHist: HistoryItem[]
|
||||||
|
trailLength: number
|
||||||
|
mounted: boolean
|
||||||
|
setIsAnimating: (value: boolean) => void
|
||||||
|
}): Promise<void> {
|
||||||
|
const { gsap, imgs, cordHist, trailLength, mounted, setIsAnimating } = args
|
||||||
|
if (!mounted) throw new Error('not mounted')
|
||||||
|
|
||||||
|
setIsAnimating(true)
|
||||||
|
|
||||||
|
const currentIndex = getCurrentElIndex(cordHist)
|
||||||
|
const trailInactiveIndexes = getTrailInactiveElsIndex(cordHist, trailLength)
|
||||||
|
|
||||||
|
lores(getImagesFromIndexes(imgs, [...trailInactiveIndexes, currentIndex]))
|
||||||
|
|
||||||
|
const tl = gsap.timeline()
|
||||||
|
const current = getImagesFromIndexes(imgs, [currentIndex])[0]
|
||||||
|
const trailInactiveEls = getImagesFromIndexes(imgs, trailInactiveIndexes)
|
||||||
|
|
||||||
|
tl.to(current, {
|
||||||
|
scale: 0.6,
|
||||||
|
duration: 0.6,
|
||||||
|
ease: 'power3.inOut'
|
||||||
|
})
|
||||||
|
tl.to(current, {
|
||||||
|
delay: 0.3,
|
||||||
|
duration: 0.7,
|
||||||
|
ease: 'power3.inOut',
|
||||||
|
x: cordHist.slice(-1)[0].x - window.innerWidth / 2,
|
||||||
|
y: cordHist.slice(-1)[0].y - window.innerHeight / 2
|
||||||
|
})
|
||||||
|
tl.to(trailInactiveEls, {
|
||||||
|
y: '-=20',
|
||||||
|
ease: 'power3.out',
|
||||||
|
stagger: -0.1,
|
||||||
|
duration: 0.3,
|
||||||
|
opacity: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
await tl.then(() => {
|
||||||
|
setIsAnimating(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,24 +1,15 @@
|
|||||||
import { For, createEffect, type Accessor, type JSX, type Setter } from 'solid-js'
|
import { For, createEffect, createMemo, on, onCleanup, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import { useState } from '../state'
|
import { useImageState } from '../imageState'
|
||||||
import { decrement, increment, type Vector } from '../utils'
|
import { decrement, increment } from '../utils'
|
||||||
|
|
||||||
import type { HistoryItem } from './layout'
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
export default function StageNav(props: {
|
export default function StageNav(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
prevText: string
|
prevText: string
|
||||||
closeText: string
|
closeText: string
|
||||||
nextText: string
|
nextText: string
|
||||||
loadingText: string
|
|
||||||
active: Accessor<boolean>
|
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
setCordHist: Setter<HistoryItem[]>
|
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
setHoverText: Setter<string>
|
|
||||||
navVector: Accessor<Vector>
|
|
||||||
setNavVector: Setter<Vector>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// types
|
// types
|
||||||
type NavItem = (typeof navItems)[number]
|
type NavItem = (typeof navItems)[number]
|
||||||
@@ -29,64 +20,74 @@ export default function StageNav(props: {
|
|||||||
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [state, { incIndex, decIndex }] = useState()
|
const imageState = useImageState()
|
||||||
|
const [
|
||||||
|
desktop,
|
||||||
|
{ incIndex, decIndex, setCordHist, setHoverText, setIsOpen, setNavVector }
|
||||||
|
] = useDesktopState()
|
||||||
|
|
||||||
const stateLength = state().length
|
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||||
|
|
||||||
const prevImage: () => void = () => {
|
const prevImage: () => void = () => {
|
||||||
props.setNavVector('prev')
|
setNavVector('prev')
|
||||||
props.setCordHist((c) =>
|
setCordHist((c) =>
|
||||||
c.map((item) => {
|
c.map((item) => {
|
||||||
return { ...item, i: decrement(item.i, stateLength) }
|
return { ...item, i: decrement(item.i, imageState().length) }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
decIndex()
|
decIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeImage: () => void = () => {
|
const closeImage: () => void = () => {
|
||||||
props.setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextImage: () => void = () => {
|
const nextImage: () => void = () => {
|
||||||
props.setNavVector('next')
|
setNavVector('next')
|
||||||
props.setCordHist((c) =>
|
setCordHist((c) =>
|
||||||
c.map((item) => {
|
c.map((item) => {
|
||||||
return { ...item, i: increment(item.i, stateLength) }
|
return { ...item, i: increment(item.i, imageState().length) }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
incIndex()
|
incIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick: (item: NavItem) => void = (item) => {
|
const handleClick: (item: NavItem) => void = (item) => {
|
||||||
if (!props.isOpen() || props.isAnimating()) return
|
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||||
if (item === navItems[0]) prevImage()
|
if (item === navItems[0]) prevImage()
|
||||||
else if (item === navItems[1]) closeImage()
|
else if (item === navItems[1]) closeImage()
|
||||||
else nextImage()
|
else nextImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
||||||
if (!props.isOpen() || props.isAnimating()) return
|
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||||
if (e.key === 'ArrowLeft') prevImage()
|
if (e.key === 'ArrowLeft') prevImage()
|
||||||
else if (e.key === 'Escape') closeImage()
|
else if (e.key === 'Escape') closeImage()
|
||||||
else if (e.key === 'ArrowRight') nextImage()
|
else if (e.key === 'ArrowRight') nextImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (props.isOpen()) {
|
on(desktop.isOpen, (isOpen) => {
|
||||||
controller = new AbortController()
|
|
||||||
const abortSignal = controller.signal
|
|
||||||
window.addEventListener('keydown', handleKey, {
|
|
||||||
passive: true,
|
|
||||||
signal: abortSignal
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
controller?.abort()
|
controller?.abort()
|
||||||
}
|
|
||||||
|
if (isOpen) {
|
||||||
|
controller = new AbortController()
|
||||||
|
const abortSignal = controller.signal
|
||||||
|
window.addEventListener('keydown', handleKey, {
|
||||||
|
passive: true,
|
||||||
|
signal: abortSignal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
controller?.abort()
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="navOverlay" classList={{ active: props.active() }}>
|
<div class="navOverlay" classList={{ active: active() }}>
|
||||||
<For each={navItems}>
|
<For each={navItems}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
@@ -94,8 +95,8 @@ export default function StageNav(props: {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClick(item)
|
handleClick(item)
|
||||||
}}
|
}}
|
||||||
onFocus={() => props.setHoverText(item)}
|
onFocus={() => setHoverText(item)}
|
||||||
onMouseOver={() => props.setHoverText(item)}
|
onMouseOver={() => setHoverText(item)}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
67
assets/ts/desktop/stageUtils.ts
Normal file
67
assets/ts/desktop/stageUtils.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { decrement, increment } from '../utils'
|
||||||
|
|
||||||
|
import type { DesktopImage } from './layout'
|
||||||
|
import type { HistoryItem } from './state'
|
||||||
|
|
||||||
|
export function getTrailElsIndex(cordHistValue: HistoryItem[]): number[] {
|
||||||
|
return cordHistValue.map((el) => el.i)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTrailInactiveElsIndex(
|
||||||
|
cordHistValue: HistoryItem[],
|
||||||
|
trailLength: number
|
||||||
|
): number[] {
|
||||||
|
return getTrailElsIndex(cordHistValue).slice(-trailLength).slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentElIndex(cordHistValue: HistoryItem[]): number {
|
||||||
|
return getTrailElsIndex(cordHistValue).slice(-1)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrevElIndex(cordHistValue: HistoryItem[], length: number): number {
|
||||||
|
return decrement(cordHistValue.slice(-1)[0].i, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextElIndex(cordHistValue: HistoryItem[], length: number): number {
|
||||||
|
return increment(cordHistValue.slice(-1)[0].i, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImagesFromIndexes(
|
||||||
|
imgs: DesktopImage[],
|
||||||
|
indexes: number[]
|
||||||
|
): DesktopImage[] {
|
||||||
|
return indexes.map((i) => imgs[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hires(imgs: DesktopImage[]): void {
|
||||||
|
imgs.forEach((img) => {
|
||||||
|
if (img.src === img.dataset.hiUrl) return
|
||||||
|
img.src = img.dataset.hiUrl
|
||||||
|
img.height = parseInt(img.dataset.hiImgH)
|
||||||
|
img.width = parseInt(img.dataset.hiImgW)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lores(imgs: DesktopImage[]): void {
|
||||||
|
imgs.forEach((img) => {
|
||||||
|
if (img.src === img.dataset.loUrl) return
|
||||||
|
img.src = img.dataset.loUrl
|
||||||
|
img.height = parseInt(img.dataset.loImgH)
|
||||||
|
img.width = parseInt(img.dataset.loImgW)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onMutation<T extends HTMLElement>(
|
||||||
|
element: T,
|
||||||
|
trigger: (arg0: MutationRecord) => boolean,
|
||||||
|
observeOptions: MutationObserverInit = { attributes: true }
|
||||||
|
): void {
|
||||||
|
new MutationObserver((mutations, observer) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (trigger(mutation)) {
|
||||||
|
observer.disconnect()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(element, observeOptions)
|
||||||
|
}
|
||||||
96
assets/ts/desktop/state.ts
Normal file
96
assets/ts/desktop/state.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
createComponent,
|
||||||
|
createContext,
|
||||||
|
createSignal,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
type JSX,
|
||||||
|
type Setter
|
||||||
|
} from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { decrement, increment, type Vector } from '../utils'
|
||||||
|
|
||||||
|
export interface HistoryItem {
|
||||||
|
i: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesktopState {
|
||||||
|
index: Accessor<number>
|
||||||
|
cordHist: Accessor<HistoryItem[]>
|
||||||
|
hoverText: Accessor<string>
|
||||||
|
isOpen: Accessor<boolean>
|
||||||
|
isAnimating: Accessor<boolean>
|
||||||
|
isLoading: Accessor<boolean>
|
||||||
|
navVector: Accessor<Vector>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DesktopStateContextType = readonly [
|
||||||
|
DesktopState,
|
||||||
|
{
|
||||||
|
readonly setIndex: Setter<number>
|
||||||
|
readonly incIndex: () => void
|
||||||
|
readonly decIndex: () => void
|
||||||
|
readonly setCordHist: Setter<HistoryItem[]>
|
||||||
|
readonly setHoverText: Setter<string>
|
||||||
|
readonly setIsOpen: Setter<boolean>
|
||||||
|
readonly setIsAnimating: Setter<boolean>
|
||||||
|
readonly setIsLoading: Setter<boolean>
|
||||||
|
readonly setNavVector: Setter<Vector>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const DesktopStateContext = createContext<DesktopStateContextType>()
|
||||||
|
|
||||||
|
export function DesktopStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||||
|
const imageState = useImageState()
|
||||||
|
|
||||||
|
const [index, setIndex] = createSignal(-1)
|
||||||
|
const [cordHist, setCordHist] = createSignal<HistoryItem[]>([])
|
||||||
|
const [hoverText, setHoverText] = createSignal('')
|
||||||
|
const [isOpen, setIsOpen] = createSignal(false)
|
||||||
|
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||||
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
|
const [navVector, setNavVector] = createSignal<Vector>('none')
|
||||||
|
|
||||||
|
const updateIndex = (stride: 1 | -1): void => {
|
||||||
|
const length = imageState().length
|
||||||
|
if (length <= 0) return
|
||||||
|
setIndex((current) =>
|
||||||
|
stride === 1 ? increment(current, length) : decrement(current, length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createComponent(DesktopStateContext.Provider, {
|
||||||
|
value: [
|
||||||
|
{ index, cordHist, hoverText, isOpen, isAnimating, isLoading, navVector },
|
||||||
|
{
|
||||||
|
setIndex,
|
||||||
|
incIndex: () => {
|
||||||
|
updateIndex(1)
|
||||||
|
},
|
||||||
|
decIndex: () => {
|
||||||
|
updateIndex(-1)
|
||||||
|
},
|
||||||
|
setCordHist,
|
||||||
|
setHoverText,
|
||||||
|
setIsOpen,
|
||||||
|
setIsAnimating,
|
||||||
|
setIsLoading,
|
||||||
|
setNavVector
|
||||||
|
}
|
||||||
|
],
|
||||||
|
get children() {
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesktopState(): DesktopStateContextType {
|
||||||
|
const context = useContext(DesktopStateContext)
|
||||||
|
invariant(context, 'undefined desktop context')
|
||||||
|
return context
|
||||||
|
}
|
||||||
41
assets/ts/imageState.tsx
Normal file
41
assets/ts/imageState.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
createMemo,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
type JSX
|
||||||
|
} from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import type { ImageJSON } from './resources'
|
||||||
|
|
||||||
|
export interface ImageState {
|
||||||
|
images: ImageJSON[]
|
||||||
|
length: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageStateContextType = Accessor<ImageState>
|
||||||
|
|
||||||
|
const ImageStateContext = createContext<ImageStateContextType>()
|
||||||
|
|
||||||
|
export function ImageStateProvider(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
images: ImageJSON[]
|
||||||
|
}): JSX.Element {
|
||||||
|
const state = createMemo<ImageState>(() => ({
|
||||||
|
images: props.images,
|
||||||
|
length: props.images.length
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageStateContext.Provider value={state}>
|
||||||
|
{props.children}
|
||||||
|
</ImageStateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImageState(): ImageStateContextType {
|
||||||
|
const context = useContext(ImageStateContext)
|
||||||
|
invariant(context, 'undefined image context')
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
import {
|
import { Match, Show, Switch, createResource, lazy, type JSX } from 'solid-js'
|
||||||
Match,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
createEffect,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
|
||||||
lazy,
|
|
||||||
type JSX
|
|
||||||
} from 'solid-js'
|
|
||||||
import { render } from 'solid-js/web'
|
import { render } from 'solid-js/web'
|
||||||
|
|
||||||
|
import { ConfigStateProvider } from './configState'
|
||||||
|
import { DesktopStateProvider } from './desktop/state'
|
||||||
|
import { ImageStateProvider } from './imageState'
|
||||||
|
import { MobileStateProvider } from './mobile/state'
|
||||||
import { getImageJSON } from './resources'
|
import { getImageJSON } from './resources'
|
||||||
import { StateProvider } from './state'
|
|
||||||
|
|
||||||
import '../scss/style.scss'
|
import '../scss/style.scss'
|
||||||
|
|
||||||
@@ -35,48 +29,60 @@ const container = document.getElementsByClassName('container')[0] as Container
|
|||||||
const Desktop = lazy(async () => await import('./desktop/layout'))
|
const Desktop = lazy(async () => await import('./desktop/layout'))
|
||||||
const Mobile = lazy(async () => await import('./mobile/layout'))
|
const Mobile = lazy(async () => await import('./mobile/layout'))
|
||||||
|
|
||||||
|
function AppContent(props: {
|
||||||
|
isMobile: boolean
|
||||||
|
prevText: string
|
||||||
|
closeText: string
|
||||||
|
nextText: string
|
||||||
|
loadingText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Switch fallback={<div>Error</div>}>
|
||||||
|
<Match when={props.isMobile}>
|
||||||
|
<MobileStateProvider>
|
||||||
|
<Mobile closeText={props.closeText} loadingText={props.loadingText} />
|
||||||
|
</MobileStateProvider>
|
||||||
|
</Match>
|
||||||
|
<Match when={!props.isMobile}>
|
||||||
|
<DesktopStateProvider>
|
||||||
|
<Desktop
|
||||||
|
prevText={props.prevText}
|
||||||
|
closeText={props.closeText}
|
||||||
|
nextText={props.nextText}
|
||||||
|
loadingText={props.loadingText}
|
||||||
|
/>
|
||||||
|
</DesktopStateProvider>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Main(): JSX.Element {
|
function Main(): JSX.Element {
|
||||||
// variables
|
// variables
|
||||||
const [ijs] = createResource(getImageJSON)
|
const [ijs] = createResource(getImageJSON)
|
||||||
const isMobile =
|
const ua = window.navigator.userAgent.toLowerCase()
|
||||||
window.matchMedia('(hover: none)').matches &&
|
const hasTouchInput = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0
|
||||||
!window.navigator.userAgent.includes('Win')
|
const hasTouchLayout =
|
||||||
|
window.matchMedia('(pointer: coarse)').matches ||
|
||||||
// states
|
window.matchMedia('(hover: none)').matches
|
||||||
const [scrollable, setScollable] = createSignal(true)
|
const isMobileUA = /android|iphone|ipad|ipod|mobile/.test(ua)
|
||||||
|
const isWindowsDesktop = /windows nt/.test(ua)
|
||||||
createEffect(() => {
|
const isMobile = isMobileUA || (hasTouchInput && hasTouchLayout && !isWindowsDesktop)
|
||||||
if (scrollable()) {
|
|
||||||
container.classList.remove('disableScroll')
|
|
||||||
} else {
|
|
||||||
container.classList.add('disableScroll')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={ijs.state === 'ready'}>
|
<Show when={ijs.state === 'ready'}>
|
||||||
<StateProvider length={ijs()?.length ?? 0}>
|
<ImageStateProvider images={ijs() ?? []}>
|
||||||
<Switch fallback={<div>Error</div>}>
|
<ConfigStateProvider>
|
||||||
<Match when={isMobile}>
|
<AppContent
|
||||||
<Mobile
|
isMobile={isMobile}
|
||||||
ijs={ijs() ?? []}
|
prevText={container.dataset.prev}
|
||||||
closeText={container.dataset.close}
|
closeText={container.dataset.close}
|
||||||
loadingText={container.dataset.loading}
|
nextText={container.dataset.next}
|
||||||
setScrollable={setScollable}
|
loadingText={container.dataset.loading}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</ConfigStateProvider>
|
||||||
<Match when={!isMobile}>
|
</ImageStateProvider>
|
||||||
<Desktop
|
|
||||||
ijs={ijs() ?? []}
|
|
||||||
prevText={container.dataset.prev}
|
|
||||||
closeText={container.dataset.close}
|
|
||||||
nextText={container.dataset.next}
|
|
||||||
loadingText={container.dataset.loading}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</StateProvider>
|
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import {
|
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
For,
|
|
||||||
createEffect,
|
|
||||||
on,
|
|
||||||
onMount,
|
|
||||||
type Accessor,
|
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import { useImageState } from '../imageState'
|
||||||
import { useState } from '../state'
|
|
||||||
|
|
||||||
import type { MobileImage } from './layout'
|
import type { MobileImage } from './layout'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
function getRandom(min: number, max: number): number {
|
function getRandom(min: number, max: number): number {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
@@ -31,29 +23,26 @@ function onIntersection<T extends HTMLElement>(
|
|||||||
}).observe(element)
|
}).observe(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Collection(props: {
|
export default function Collection(): JSX.Element {
|
||||||
children?: JSX.Element
|
|
||||||
ijs: ImageJSON[]
|
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
|
||||||
// variables
|
// variables
|
||||||
// eslint-disable-next-line solid/reactivity
|
const imageState = useImageState()
|
||||||
const imgs: MobileImage[] = Array<MobileImage>(props.ijs.length)
|
const imgs: MobileImage[] = Array<MobileImage>(imageState().length)
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [state, { setIndex }] = useState()
|
const [mobile, { setIndex, setIsOpen }] = useMobileState()
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
const handleClick: (i: number) => void = (i) => {
|
const handleClick: (i: number) => void = (i) => {
|
||||||
if (props.isAnimating()) return
|
if (mobile.isAnimating()) return
|
||||||
setIndex(i)
|
setIndex(i)
|
||||||
props.setIsOpen(true)
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToActive: () => void = () => {
|
const scrollToActive: () => void = () => {
|
||||||
imgs[state().index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
const index = mobile.index()
|
||||||
|
|
||||||
|
if (index < 0) return
|
||||||
|
imgs[index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// effects
|
// effects
|
||||||
@@ -94,11 +83,9 @@ export default function Collection(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
|
mobile.isOpen,
|
||||||
() => {
|
() => {
|
||||||
props.isOpen()
|
if (!mobile.isOpen()) scrollToActive() // scroll to active when closed
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (!props.isOpen()) scrollToActive() // scroll to active when closed
|
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true }
|
||||||
)
|
)
|
||||||
@@ -107,7 +94,7 @@ export default function Collection(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="collection">
|
<div class="collection">
|
||||||
<For each={props.ijs}>
|
<For each={imageState().images}>
|
||||||
{(ij, i) => (
|
{(ij, i) => (
|
||||||
<img
|
<img
|
||||||
ref={imgs[i()]}
|
ref={imgs[i()]}
|
||||||
|
|||||||
@@ -1,209 +1,170 @@
|
|||||||
import { type gsap } from 'gsap'
|
import { type gsap } from 'gsap'
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
|
createMemo,
|
||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
Show,
|
untrack,
|
||||||
type Accessor,
|
type JSX
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
} from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { type Swiper } from 'swiper'
|
import { type Swiper } from 'swiper'
|
||||||
import invariant from 'tiny-invariant'
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
import { type ImageJSON } from '../resources'
|
import { useImageState } from '../imageState'
|
||||||
import { useState } from '../state'
|
import { loadGsap, removeDuplicates, type Vector } from '../utils'
|
||||||
import { loadGsap, type Vector } from '../utils'
|
|
||||||
|
|
||||||
import GalleryImage from './galleryImage'
|
import GalleryImage from './galleryImage'
|
||||||
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
||||||
|
import { closeGallery, openGallery } from './galleryTransitions'
|
||||||
function removeDuplicates<T>(arr: T[]): T[] {
|
import { getActiveImageIndexes, loadSwiper } from './galleryUtils'
|
||||||
if (arr.length < 2) return arr // optimization
|
import { useMobileState } from './state'
|
||||||
return [...new Set(arr)]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSwiper(): Promise<typeof Swiper> {
|
|
||||||
const s = await import('swiper')
|
|
||||||
return s.Swiper
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Gallery(props: {
|
export default function Gallery(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
ijs: ImageJSON[]
|
|
||||||
closeText: string
|
closeText: string
|
||||||
loadingText: string
|
loadingText: string
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
setIsAnimating: Setter<boolean>
|
|
||||||
isOpen: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
setScrollable: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// variables
|
|
||||||
let _gsap: typeof gsap
|
let _gsap: typeof gsap
|
||||||
let _swiper: Swiper
|
let _swiper: Swiper | undefined
|
||||||
|
let initPromise: Promise<void> | undefined
|
||||||
|
|
||||||
let curtain: HTMLDivElement | undefined
|
let curtain: HTMLDivElement | undefined
|
||||||
let gallery: HTMLDivElement | undefined
|
let gallery: HTMLDivElement | undefined
|
||||||
let galleryInner: HTMLDivElement | undefined
|
let galleryInner: HTMLDivElement | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
const imageState = useImageState()
|
||||||
const _loadingText = capitalizeFirstLetter(props.loadingText)
|
const [mobile, { setIndex, setIsAnimating, setIsScrollLocked }] = useMobileState()
|
||||||
|
|
||||||
|
const loadingText = createMemo(() => capitalizeFirstLetter(props.loadingText))
|
||||||
|
|
||||||
// states
|
|
||||||
let lastIndex = -1
|
let lastIndex = -1
|
||||||
let mounted = false
|
let mounted = false
|
||||||
let navigateVector: Vector = 'none'
|
let navigateVector: Vector = 'none'
|
||||||
|
|
||||||
const [state, { setIndex }] = useState()
|
|
||||||
const [libLoaded, setLibLoaded] = createSignal(false)
|
const [libLoaded, setLibLoaded] = createSignal(false)
|
||||||
// eslint-disable-next-line solid/reactivity
|
const [swiperReady, setSwiperReady] = createSignal(false)
|
||||||
const [loads, setLoads] = createStore(Array<boolean>(props.ijs.length).fill(false))
|
const [loads, setLoads] = createStore(Array<boolean>(imageState().length).fill(false))
|
||||||
|
|
||||||
// helper functions
|
|
||||||
const slideUp: () => void = () => {
|
const slideUp: () => void = () => {
|
||||||
// isAnimating is prechecked in isOpen effect
|
|
||||||
if (!libLoaded() || !mounted) return
|
if (!libLoaded() || !mounted) return
|
||||||
props.setIsAnimating(true)
|
|
||||||
|
|
||||||
invariant(curtain, 'curtain is not defined')
|
invariant(curtain, 'curtain is not defined')
|
||||||
invariant(gallery, 'gallery is not defined')
|
invariant(gallery, 'gallery is not defined')
|
||||||
|
|
||||||
_gsap.to(curtain, {
|
openGallery({
|
||||||
opacity: 1,
|
gsap: _gsap,
|
||||||
duration: 1
|
curtain,
|
||||||
|
gallery,
|
||||||
|
setIsAnimating,
|
||||||
|
setIsScrollLocked
|
||||||
})
|
})
|
||||||
|
|
||||||
_gsap.to(gallery, {
|
|
||||||
y: 0,
|
|
||||||
ease: 'power3.inOut',
|
|
||||||
duration: 1,
|
|
||||||
delay: 0.4
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
props.setScrollable(false)
|
|
||||||
props.setIsAnimating(false)
|
|
||||||
}, 1200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const slideDown: () => void = () => {
|
const slideDown: () => void = () => {
|
||||||
// isAnimating is prechecked in isOpen effect
|
|
||||||
props.setIsAnimating(true)
|
|
||||||
|
|
||||||
invariant(gallery, 'curtain is not defined')
|
invariant(gallery, 'curtain is not defined')
|
||||||
invariant(curtain, 'gallery is not defined')
|
invariant(curtain, 'gallery is not defined')
|
||||||
|
|
||||||
_gsap.to(gallery, {
|
closeGallery({
|
||||||
y: '100%',
|
gsap: _gsap,
|
||||||
ease: 'power3.inOut',
|
curtain,
|
||||||
duration: 1
|
gallery,
|
||||||
|
setIsAnimating,
|
||||||
|
setIsScrollLocked,
|
||||||
|
onClosed: () => {
|
||||||
|
lastIndex = -1
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
_gsap.to(curtain, {
|
|
||||||
opacity: 0,
|
|
||||||
duration: 1.2,
|
|
||||||
delay: 0.4
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// cleanup
|
|
||||||
props.setScrollable(true)
|
|
||||||
props.setIsAnimating(false)
|
|
||||||
lastIndex = -1
|
|
||||||
}, 1400)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const galleryLoadImages: () => void = () => {
|
const galleryLoadImages: () => void = () => {
|
||||||
let activeImagesIndex: number[] = []
|
const currentIndex = mobile.index()
|
||||||
const _state = state()
|
|
||||||
const currentIndex = _state.index
|
setLoads(
|
||||||
const nextIndex = Math.min(currentIndex + 1, _state.length - 1)
|
removeDuplicates(
|
||||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
getActiveImageIndexes(currentIndex, imageState().length, navigateVector)
|
||||||
switch (navigateVector) {
|
),
|
||||||
case 'next':
|
true
|
||||||
activeImagesIndex = [nextIndex]
|
)
|
||||||
break
|
|
||||||
case 'prev':
|
|
||||||
activeImagesIndex = [prevIndex]
|
|
||||||
break
|
|
||||||
case 'none':
|
|
||||||
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
setLoads(removeDuplicates(activeImagesIndex), true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeSlide: (slide: number) => void = (slide) => {
|
const changeSlide: (slide: number) => void = (slide) => {
|
||||||
// we are already in the gallery, don't need to
|
if (!swiperReady() || _swiper === undefined) return
|
||||||
// check mounted or libLoaded
|
|
||||||
galleryLoadImages()
|
galleryLoadImages()
|
||||||
_swiper.slideTo(slide, 0)
|
_swiper.slideTo(slide, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// effects
|
const ensureGalleryReady: () => Promise<void> = async () => {
|
||||||
onMount(() => {
|
if (initPromise !== undefined) return await initPromise
|
||||||
window.addEventListener(
|
|
||||||
'touchstart',
|
initPromise = (async () => {
|
||||||
() => {
|
try {
|
||||||
loadGsap()
|
const [g, S] = await Promise.all([loadGsap(), loadSwiper()])
|
||||||
.then((g) => {
|
|
||||||
_gsap = g
|
_gsap = g
|
||||||
})
|
|
||||||
.catch((e) => {
|
invariant(galleryInner, 'galleryInner is not defined')
|
||||||
console.log(e)
|
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||||
})
|
_swiper.on('slideChange', ({ realIndex }) => {
|
||||||
loadSwiper()
|
setIndex(realIndex)
|
||||||
.then((S) => {
|
})
|
||||||
invariant(galleryInner, 'galleryInner is not defined')
|
|
||||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
|
||||||
_swiper.on('slideChange', ({ realIndex }) => {
|
|
||||||
setIndex(realIndex)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
setLibLoaded(true)
|
setLibLoaded(true)
|
||||||
},
|
setSwiperReady(true)
|
||||||
{ once: true, passive: true }
|
|
||||||
)
|
const initialIndex = untrack(mobile.index)
|
||||||
|
|
||||||
|
if (initialIndex >= 0) {
|
||||||
|
changeSlide(initialIndex)
|
||||||
|
lastIndex = initialIndex
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
initPromise = undefined
|
||||||
|
setSwiperReady(false)
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
await initPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('touchstart', () => void ensureGalleryReady(), {
|
||||||
|
once: true,
|
||||||
|
passive: true
|
||||||
|
})
|
||||||
mounted = true
|
mounted = true
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => {
|
() => [swiperReady(), mobile.index()] as const,
|
||||||
state()
|
([ready, index]) => {
|
||||||
},
|
if (!ready || index < 0) return
|
||||||
() => {
|
if (index === lastIndex) return
|
||||||
const i = state().index
|
if (lastIndex === -1) navigateVector = 'none'
|
||||||
if (i === lastIndex)
|
else if (index < lastIndex) navigateVector = 'prev'
|
||||||
return // change slide only when index is changed
|
else if (index > lastIndex) navigateVector = 'next'
|
||||||
else if (lastIndex === -1)
|
else navigateVector = 'none'
|
||||||
navigateVector = 'none' // lastIndex before set
|
changeSlide(index)
|
||||||
else if (i < lastIndex)
|
lastIndex = index
|
||||||
navigateVector = 'prev' // set navigate vector for galleryLoadImages
|
|
||||||
else if (i > lastIndex)
|
|
||||||
navigateVector = 'next' // set navigate vector for galleryLoadImages
|
|
||||||
else navigateVector = 'none' // default
|
|
||||||
changeSlide(i) // change slide to new index
|
|
||||||
lastIndex = i // update last index
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => {
|
() => mobile.isOpen(),
|
||||||
props.isOpen()
|
async (isOpen) => {
|
||||||
},
|
if (isOpen && !swiperReady()) {
|
||||||
() => {
|
await ensureGalleryReady()
|
||||||
if (props.isAnimating()) return
|
}
|
||||||
if (props.isOpen()) slideUp()
|
|
||||||
|
if (!libLoaded() || !swiperReady()) return
|
||||||
|
if (mobile.isAnimating()) return
|
||||||
|
if (isOpen) slideUp()
|
||||||
else slideDown()
|
else slideDown()
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true }
|
||||||
@@ -215,26 +176,16 @@ export default function Gallery(props: {
|
|||||||
<div ref={gallery} class="gallery">
|
<div ref={gallery} class="gallery">
|
||||||
<div ref={galleryInner} class="galleryInner">
|
<div ref={galleryInner} class="galleryInner">
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper">
|
||||||
<Show when={libLoaded()}>
|
<For each={imageState().images}>
|
||||||
<For each={props.ijs}>
|
{(ij, i) => (
|
||||||
{(ij, i) => (
|
<div class="swiper-slide">
|
||||||
<div class="swiper-slide">
|
<GalleryImage load={loads[i()]} ij={ij} loadingText={loadingText()} />
|
||||||
<GalleryImage
|
</div>
|
||||||
load={loads[i()]}
|
)}
|
||||||
ij={ij}
|
</For>
|
||||||
loadingText={_loadingText}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GalleryNav
|
<GalleryNav closeText={props.closeText} />
|
||||||
closeText={props.closeText}
|
|
||||||
isAnimating={props.isAnimating}
|
|
||||||
setIsOpen={props.setIsOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div ref={curtain} class="curtain" />
|
<div ref={curtain} class="curtain" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { onMount, type JSX } from 'solid-js'
|
import { type gsap } from 'gsap'
|
||||||
|
import { createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
import invariant from 'tiny-invariant'
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import type { ImageJSON } from '../resources'
|
||||||
import { useState } from '../state'
|
|
||||||
import { loadGsap } from '../utils'
|
import { loadGsap } from '../utils'
|
||||||
|
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
export default function GalleryImage(props: {
|
export default function GalleryImage(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
load: boolean
|
load: boolean
|
||||||
@@ -14,40 +16,83 @@ export default function GalleryImage(props: {
|
|||||||
let img: HTMLImageElement | undefined
|
let img: HTMLImageElement | undefined
|
||||||
let loadingDiv: HTMLDivElement | undefined
|
let loadingDiv: HTMLDivElement | undefined
|
||||||
|
|
||||||
let _gsap: typeof gsap
|
let _gsap: typeof gsap | undefined
|
||||||
|
let gsapPromise: Promise<typeof gsap> | undefined
|
||||||
|
let revealed = false
|
||||||
|
|
||||||
const [state] = useState()
|
const [mobile] = useMobileState()
|
||||||
|
|
||||||
|
const revealImage = async (): Promise<void> => {
|
||||||
|
if (revealed) return
|
||||||
|
revealed = true
|
||||||
|
|
||||||
|
invariant(img, 'ref must be defined')
|
||||||
|
invariant(loadingDiv, 'loadingDiv must be defined')
|
||||||
|
|
||||||
|
gsapPromise ??= loadGsap()
|
||||||
|
|
||||||
|
try {
|
||||||
|
_gsap ??= await gsapPromise
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_gsap === undefined) {
|
||||||
|
img.style.opacity = '1'
|
||||||
|
loadingDiv.style.opacity = '0'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobile.index() !== props.ij.index) {
|
||||||
|
_gsap.set(img, { opacity: 1 })
|
||||||
|
_gsap.set(loadingDiv, { opacity: 0 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_gsap.to(img, {
|
||||||
|
opacity: 1,
|
||||||
|
delay: 0.5,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: 'power3.out'
|
||||||
|
})
|
||||||
|
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadGsap()
|
gsapPromise = loadGsap()
|
||||||
.then((g) => {
|
.then((g) => {
|
||||||
_gsap = g
|
_gsap = g
|
||||||
|
return g
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
throw e
|
||||||
})
|
})
|
||||||
|
|
||||||
img?.addEventListener(
|
img?.addEventListener(
|
||||||
'load',
|
'load',
|
||||||
() => {
|
() => {
|
||||||
invariant(img, 'ref must be defined')
|
void revealImage()
|
||||||
invariant(loadingDiv, 'loadingDiv must be defined')
|
|
||||||
if (state().index !== props.ij.index) {
|
|
||||||
_gsap.set(img, { opacity: 1 })
|
|
||||||
_gsap.set(loadingDiv, { opacity: 0 })
|
|
||||||
} else {
|
|
||||||
_gsap.to(img, {
|
|
||||||
opacity: 1,
|
|
||||||
delay: 0.5,
|
|
||||||
duration: 0.5,
|
|
||||||
ease: 'power3.out'
|
|
||||||
})
|
|
||||||
_gsap.to(loadingDiv, { opacity: 0, duration: 0.5, ease: 'power3.in' })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ once: true, passive: true }
|
{ once: true, passive: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (props.load && img?.complete && img.currentSrc !== '') {
|
||||||
|
void revealImage()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => props.load,
|
||||||
|
(load) => {
|
||||||
|
if (!load || img === undefined || !img.complete || img.currentSrc === '') return
|
||||||
|
void revealImage()
|
||||||
|
},
|
||||||
|
{ defer: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="slideContainer">
|
<div class="slideContainer">
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createMemo, type Accessor, type JSX, type Setter } from 'solid-js'
|
import { createMemo, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import { useState } from '../state'
|
import { useImageState } from '../imageState'
|
||||||
import { expand } from '../utils'
|
import { expand } from '../utils'
|
||||||
|
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
export function capitalizeFirstLetter(str: string): string {
|
export function capitalizeFirstLetter(str: string): string {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
}
|
}
|
||||||
@@ -10,17 +12,16 @@ export function capitalizeFirstLetter(str: string): string {
|
|||||||
export default function GalleryNav(props: {
|
export default function GalleryNav(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
closeText: string
|
closeText: string
|
||||||
isAnimating: Accessor<boolean>
|
|
||||||
setIsOpen: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// states
|
// states
|
||||||
const [state] = useState()
|
const imageState = useImageState()
|
||||||
const indexValue = createMemo(() => expand(state().index + 1))
|
const [mobile, { setIsOpen }] = useMobileState()
|
||||||
const indexLength = createMemo(() => expand(state().length))
|
const indexValue = createMemo(() => expand(mobile.index() + 1))
|
||||||
|
const indexLength = createMemo(() => expand(imageState().length))
|
||||||
|
|
||||||
const onClick: () => void = () => {
|
const onClick: () => void = () => {
|
||||||
if (props.isAnimating()) return
|
if (mobile.isAnimating()) return
|
||||||
props.setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,7 +38,14 @@ export default function GalleryNav(props: {
|
|||||||
<span class="num">{indexLength()[2]}</span>
|
<span class="num">{indexLength()[2]}</span>
|
||||||
<span class="num">{indexLength()[3]}</span>
|
<span class="num">{indexLength()[3]}</span>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={onClick} onKeyDown={onClick}>
|
<div
|
||||||
|
class="navClose"
|
||||||
|
onClick={onClick}
|
||||||
|
onTouchEnd={onClick}
|
||||||
|
onKeyDown={onClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
>
|
||||||
{capitalizeFirstLetter(props.closeText)}
|
{capitalizeFirstLetter(props.closeText)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
64
assets/ts/mobile/galleryTransitions.ts
Normal file
64
assets/ts/mobile/galleryTransitions.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { type gsap } from 'gsap'
|
||||||
|
|
||||||
|
const OPEN_DELAY_MS = 1200
|
||||||
|
const CLOSE_DELAY_MS = 1400
|
||||||
|
|
||||||
|
export function openGallery(args: {
|
||||||
|
gsap: typeof gsap
|
||||||
|
curtain: HTMLDivElement
|
||||||
|
gallery: HTMLDivElement
|
||||||
|
setIsAnimating: (value: boolean) => void
|
||||||
|
setIsScrollLocked: (value: boolean) => void
|
||||||
|
}): void {
|
||||||
|
const { gsap, curtain, gallery, setIsAnimating, setIsScrollLocked } = args
|
||||||
|
|
||||||
|
setIsAnimating(true)
|
||||||
|
|
||||||
|
gsap.to(curtain, {
|
||||||
|
opacity: 1,
|
||||||
|
duration: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
gsap.to(gallery, {
|
||||||
|
y: 0,
|
||||||
|
ease: 'power3.inOut',
|
||||||
|
duration: 1,
|
||||||
|
delay: 0.4
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsScrollLocked(true)
|
||||||
|
setIsAnimating(false)
|
||||||
|
}, OPEN_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeGallery(args: {
|
||||||
|
gsap: typeof gsap
|
||||||
|
curtain: HTMLDivElement
|
||||||
|
gallery: HTMLDivElement
|
||||||
|
setIsAnimating: (value: boolean) => void
|
||||||
|
setIsScrollLocked: (value: boolean) => void
|
||||||
|
onClosed: () => void
|
||||||
|
}): void {
|
||||||
|
const { gsap, curtain, gallery, setIsAnimating, setIsScrollLocked, onClosed } = args
|
||||||
|
|
||||||
|
setIsAnimating(true)
|
||||||
|
|
||||||
|
gsap.to(gallery, {
|
||||||
|
y: '100%',
|
||||||
|
ease: 'power3.inOut',
|
||||||
|
duration: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
gsap.to(curtain, {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 1.2,
|
||||||
|
delay: 0.4
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsScrollLocked(false)
|
||||||
|
setIsAnimating(false)
|
||||||
|
onClosed()
|
||||||
|
}, CLOSE_DELAY_MS)
|
||||||
|
}
|
||||||
26
assets/ts/mobile/galleryUtils.ts
Normal file
26
assets/ts/mobile/galleryUtils.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { type Swiper } from 'swiper'
|
||||||
|
|
||||||
|
import type { Vector } from '../utils'
|
||||||
|
|
||||||
|
export async function loadSwiper(): Promise<typeof Swiper> {
|
||||||
|
const swiper = await import('swiper')
|
||||||
|
return swiper.Swiper
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveImageIndexes(
|
||||||
|
currentIndex: number,
|
||||||
|
length: number,
|
||||||
|
navigateVector: Vector
|
||||||
|
): number[] {
|
||||||
|
const nextIndex = Math.min(currentIndex + 1, length - 1)
|
||||||
|
const prevIndex = Math.max(currentIndex - 1, 0)
|
||||||
|
|
||||||
|
switch (navigateVector) {
|
||||||
|
case 'next':
|
||||||
|
return [nextIndex]
|
||||||
|
case 'prev':
|
||||||
|
return [prevIndex]
|
||||||
|
case 'none':
|
||||||
|
return [currentIndex, nextIndex, prevIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Show, createSignal, type JSX, type Setter } from 'solid-js'
|
import { Show, createEffect, onCleanup, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import type { ImageJSON } from '../resources'
|
import { useImageState } from '../imageState'
|
||||||
|
|
||||||
import Collection from './collection'
|
import Collection from './collection'
|
||||||
import Gallery from './gallery'
|
import Gallery from './gallery'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interfaces
|
* interfaces
|
||||||
@@ -18,34 +19,33 @@ export interface MobileImage extends HTMLImageElement {
|
|||||||
|
|
||||||
export default function Mobile(props: {
|
export default function Mobile(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
ijs: ImageJSON[]
|
|
||||||
closeText: string
|
closeText: string
|
||||||
loadingText: string
|
loadingText: string
|
||||||
setScrollable: Setter<boolean>
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// states
|
const imageState = useImageState()
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
const [mobile] = useMobileState()
|
||||||
const [isAnimating, setIsAnimating] = createSignal(false)
|
|
||||||
|
createEffect(() => {
|
||||||
|
const container = document.getElementsByClassName('container').item(0)
|
||||||
|
if (container === null) return
|
||||||
|
|
||||||
|
if (mobile.isScrollLocked()) {
|
||||||
|
container.classList.add('disableScroll')
|
||||||
|
} else {
|
||||||
|
container.classList.remove('disableScroll')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
const container = document.getElementsByClassName('container').item(0)
|
||||||
|
container?.classList.remove('disableScroll')
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={props.ijs.length > 0}>
|
<Show when={imageState().length > 0}>
|
||||||
<Collection
|
<Collection />
|
||||||
ijs={props.ijs}
|
<Gallery closeText={props.closeText} loadingText={props.loadingText} />
|
||||||
isAnimating={isAnimating}
|
|
||||||
isOpen={isOpen}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
/>
|
|
||||||
<Gallery
|
|
||||||
ijs={props.ijs}
|
|
||||||
closeText={props.closeText}
|
|
||||||
loadingText={props.loadingText}
|
|
||||||
isAnimating={isAnimating}
|
|
||||||
setIsAnimating={setIsAnimating}
|
|
||||||
isOpen={isOpen}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
setScrollable={props.setScrollable}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
78
assets/ts/mobile/state.ts
Normal file
78
assets/ts/mobile/state.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
createComponent,
|
||||||
|
createContext,
|
||||||
|
createSignal,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
type JSX,
|
||||||
|
type Setter
|
||||||
|
} from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { decrement, increment } from '../utils'
|
||||||
|
|
||||||
|
export interface MobileState {
|
||||||
|
index: Accessor<number>
|
||||||
|
isOpen: Accessor<boolean>
|
||||||
|
isAnimating: Accessor<boolean>
|
||||||
|
isScrollLocked: Accessor<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MobileStateContextType = readonly [
|
||||||
|
MobileState,
|
||||||
|
{
|
||||||
|
readonly setIndex: Setter<number>
|
||||||
|
readonly incIndex: () => void
|
||||||
|
readonly decIndex: () => void
|
||||||
|
readonly setIsOpen: Setter<boolean>
|
||||||
|
readonly setIsAnimating: Setter<boolean>
|
||||||
|
readonly setIsScrollLocked: Setter<boolean>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const MobileStateContext = createContext<MobileStateContextType>()
|
||||||
|
|
||||||
|
export function MobileStateProvider(props: { children?: JSX.Element }): JSX.Element {
|
||||||
|
const imageState = useImageState()
|
||||||
|
|
||||||
|
const [index, setIndex] = createSignal(-1)
|
||||||
|
const [isOpen, setIsOpen] = createSignal(false)
|
||||||
|
const [isAnimating, setIsAnimating] = createSignal(false)
|
||||||
|
const [isScrollLocked, setIsScrollLocked] = createSignal(false)
|
||||||
|
|
||||||
|
const updateIndex = (stride: 1 | -1): void => {
|
||||||
|
const length = imageState().length
|
||||||
|
if (length <= 0) return
|
||||||
|
setIndex((current) =>
|
||||||
|
stride === 1 ? increment(current, length) : decrement(current, length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createComponent(MobileStateContext.Provider, {
|
||||||
|
value: [
|
||||||
|
{ index, isOpen, isAnimating, isScrollLocked },
|
||||||
|
{
|
||||||
|
setIndex,
|
||||||
|
incIndex: () => {
|
||||||
|
updateIndex(1)
|
||||||
|
},
|
||||||
|
decIndex: () => {
|
||||||
|
updateIndex(-1)
|
||||||
|
},
|
||||||
|
setIsOpen,
|
||||||
|
setIsAnimating,
|
||||||
|
setIsScrollLocked
|
||||||
|
}
|
||||||
|
],
|
||||||
|
get children() {
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMobileState(): MobileStateContextType {
|
||||||
|
const context = useContext(MobileStateContext)
|
||||||
|
invariant(context, 'undefined mobile context')
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
createSignal,
|
|
||||||
useContext,
|
|
||||||
type Accessor,
|
|
||||||
type JSX,
|
|
||||||
type Setter
|
|
||||||
} from 'solid-js'
|
|
||||||
import invariant from 'tiny-invariant'
|
|
||||||
|
|
||||||
import { decrement, getThresholdSessionIndex, increment } from './utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* interfaces and types
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ThresholdRelated {
|
|
||||||
threshold: number
|
|
||||||
trailLength: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
index: number
|
|
||||||
length: number
|
|
||||||
threshold: number
|
|
||||||
trailLength: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StateContextType = readonly [
|
|
||||||
Accessor<State>,
|
|
||||||
{
|
|
||||||
readonly setIndex: (index: number) => void
|
|
||||||
readonly incIndex: () => void
|
|
||||||
readonly decIndex: () => void
|
|
||||||
readonly incThreshold: () => void
|
|
||||||
readonly decThreshold: () => void
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constants
|
|
||||||
*/
|
|
||||||
|
|
||||||
const thresholds: ThresholdRelated[] = [
|
|
||||||
{ threshold: 20, trailLength: 20 },
|
|
||||||
{ threshold: 40, trailLength: 10 },
|
|
||||||
{ threshold: 80, trailLength: 5 },
|
|
||||||
{ threshold: 140, trailLength: 5 },
|
|
||||||
{ threshold: 200, trailLength: 5 }
|
|
||||||
]
|
|
||||||
const makeStateContext: (
|
|
||||||
state: Accessor<State>,
|
|
||||||
setState: Setter<State>
|
|
||||||
) => StateContextType = (state: Accessor<State>, setState: Setter<State>) => {
|
|
||||||
return [
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
setIndex: (index: number) => {
|
|
||||||
setState((s) => {
|
|
||||||
return { ...s, index }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
incIndex: () => {
|
|
||||||
setState((s) => {
|
|
||||||
return { ...s, index: increment(s.index, s.length) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
decIndex: () => {
|
|
||||||
setState((s) => {
|
|
||||||
return { ...s, index: decrement(s.index, s.length) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
incThreshold: () => {
|
|
||||||
setState((s) => {
|
|
||||||
return { ...s, ...updateThreshold(s.threshold, thresholds, 1) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
decThreshold: () => {
|
|
||||||
setState((s) => {
|
|
||||||
return { ...s, ...updateThreshold(s.threshold, thresholds, -1) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
] as const
|
|
||||||
}
|
|
||||||
const StateContext = createContext<StateContextType>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function updateThreshold(
|
|
||||||
currentThreshold: number,
|
|
||||||
thresholds: ThresholdRelated[],
|
|
||||||
stride: number
|
|
||||||
): ThresholdRelated {
|
|
||||||
const i = thresholds.findIndex((t) => t.threshold === currentThreshold) + stride
|
|
||||||
if (i < 0 || i >= thresholds.length) return thresholds[i - stride]
|
|
||||||
// storage the index so we can restore it even if we go to another page
|
|
||||||
sessionStorage.setItem('thresholdsIndex', i.toString())
|
|
||||||
return thresholds[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* StateProvider
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function StateProvider(props: {
|
|
||||||
children?: JSX.Element
|
|
||||||
length: number
|
|
||||||
}): JSX.Element {
|
|
||||||
const defaultState: State = {
|
|
||||||
index: -1,
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
length: props.length,
|
|
||||||
threshold: thresholds[getThresholdSessionIndex()].threshold,
|
|
||||||
trailLength: thresholds[getThresholdSessionIndex()].trailLength
|
|
||||||
}
|
|
||||||
|
|
||||||
const [state, setState] = createSignal(defaultState)
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
const contextValue = makeStateContext(state, setState)
|
|
||||||
return (
|
|
||||||
<StateContext.Provider value={contextValue}>{props.children}</StateContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* use context
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function useState(): StateContextType {
|
|
||||||
const uc = useContext(StateContext)
|
|
||||||
invariant(uc, 'undefined context')
|
|
||||||
return uc
|
|
||||||
}
|
|
||||||
1
bundled/css/critical.css
Normal file
1
bundled/css/critical.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*:where(:not(html,iframe,canvas,img,svg,video,audio):not(svg *,symbol *)){all:unset;display:revert}*,*:before,*:after{box-sizing:border-box}html{-moz-text-size-adjust:none;-webkit-text-size-adjust:none;text-size-adjust:none}a,button{cursor:revert}ol,ul,menu,summary{list-style:none}img{max-inline-size:100%;max-block-size:100%}table{border-collapse:collapse}input,textarea{-webkit-user-select:auto}textarea{white-space:revert}meter{-webkit-appearance:revert;appearance:revert}:where(pre){all:revert;box-sizing:border-box}::placeholder{color:unset}:where([hidden]){display:none}:where([contenteditable]:not([contenteditable=false])){-moz-user-modify:read-write;-webkit-user-modify:read-write;overflow-wrap:break-word;-webkit-line-break:after-white-space;-webkit-user-select:auto}:where([draggable=true]){-webkit-user-drag:element}:where(dialog:modal){all:revert;box-sizing:border-box}@font-face{font-family:Geist;src:url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}') format("woff2 supports variations"),url('{{- "lib/fonts/GeistVF.woff2" | absURL -}}') format("woff2-variations");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:FW;src:url('{{- "lib/fonts/fw.woff2" | absURL -}}') format("woff2");font-weight:400;font-style:normal;font-display:swap}body{line-height:1.2;font-size:16px;font-family:Geist,sans-serif}body button{font-family:FW,sans-serif}@media(min-width:768px){body{font-size:18px}}@media(min-width:1024px){body{font-size:19px}}:root{--window-height: 100vh;--nav-height: 2rem;--space-standard: .625rem;--z-curtain: 200;--z-nav-gallery: 500;--z-cursor: 600;--z-nav: 800}*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{-webkit-user-select:none;user-select:none;background:#fff}html,body{overscroll-behavior-y:none}a,button{cursor:pointer}nav{display:flex;justify-content:space-between;align-items:center;width:100%;height:var(--nav-height);padding:0 var(--space-standard);position:fixed;bottom:0;background:#fff;z-index:var(--z-nav);pointer-events:all}.num{width:.625em;display:inline-block;text-align:center}.current{font-style:italic;text-decoration:underline}@media(max-width:767px),(hover:none){nav{top:0}.index,.threshold{display:none}}article{padding:var(--space-standard);max-width:25em}article p{margin-bottom:1em}article u{text-decoration:underline}article>h1{font-size:1.6em}article>h2{font-size:1.5em}article>h3{font-size:1.375em}article>h4{font-size:1.25em}article>h5{font-size:1.125em}article h1,article h2,article h3,article h4,article h5,article h6{font-weight:700;margin:1.2rem 0}@media(max-width:767px),(hover:none){article{margin-top:var(--nav-height)}}@media(max-width:767px),(hover:none){.container{position:fixed;top:0;z-index:0;width:100vw;height:var(--window-height);overflow-y:scroll;overflow-x:hidden;background:#fff;overscroll-behavior:none;-webkit-overflow-scrolling:none}.disableScroll{pointer-events:none}}
|
||||||
1
bundled/css/main.css
Normal file
1
bundled/css/main.css
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/CO8Cxe.js
Normal file
1
bundled/js/CO8Cxe.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/DaqdZh.js
Normal file
1
bundled/js/DaqdZh.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/fZjYgW.js
Normal file
1
bundled/js/fZjYgW.js
Normal file
File diff suppressed because one or more lines are too long
1
bundled/js/h6I38a.js
Normal file
1
bundled/js/h6I38a.js
Normal file
File diff suppressed because one or more lines are too long
2
bundled/js/main.js
Normal file
2
bundled/js/main.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["js/fZjYgW.js","js/h6I38a.js","js/zA1TQP.js"])))=>i.map(i=>d[i]);
|
||||||
|
import{B as e,C as t,D as n,E as r,M as i,P as a,S as o,_ as s,a as c,j as l,k as u,l as d,n as f,o as p,p as m,r as h,t as g,u as _,w as v}from"./h6I38a.js";var y=[{threshold:20,trailLength:20},{threshold:40,trailLength:10},{threshold:80,trailLength:5},{threshold:140,trailLength:5},{threshold:200,trailLength:5}],b=n();function x(){let e=c();return e<0||e>=y.length?2:e}function S(e){let[t,n]=i(x()),a=u(()=>{let e=y[t()];return{thresholdIndex:t(),threshold:e.threshold,trailLength:e.trailLength}}),o=e=>{let r=t()+e;r<0||r>=y.length||(sessionStorage.setItem(`thresholdsIndex`,r.toString()),n(r))};return r(b.Provider,{value:[a,{incThreshold:()=>{o(1)},decThreshold:()=>{o(-1)}}],get children(){return e.children}})}function C(){let t=e(b);return _(t,`undefined config context`),t}var w=n();function T(e){let t=f(),[n,a]=i(-1),[o,s]=i([]),[c,l]=i(``),[u,d]=i(!1),[m,g]=i(!1),[_,v]=i(!1),[y,b]=i(`none`),x=e=>{let n=t().length;n<=0||a(t=>e===1?p(t,n):h(t,n))};return r(w.Provider,{value:[{index:n,cordHist:o,hoverText:c,isOpen:u,isAnimating:m,isLoading:_,navVector:y},{setIndex:a,incIndex:()=>{x(1)},decIndex:()=>{x(-1)},setCordHist:s,setHoverText:l,setIsOpen:d,setIsAnimating:g,setIsLoading:v,setNavVector:b}],get children(){return e.children}})}function E(){let t=e(w);return _(t,`undefined desktop context`),t}var D=n();function O(e){let t=f(),[n,a]=i(-1),[o,s]=i(!1),[c,l]=i(!1),[u,d]=i(!1),m=e=>{let n=t().length;n<=0||a(t=>e===1?p(t,n):h(t,n))};return r(D.Provider,{value:[{index:n,isOpen:o,isAnimating:c,isScrollLocked:u},{setIndex:a,incIndex:()=>{m(1)},decIndex:()=>{m(-1)},setIsOpen:s,setIsAnimating:l,setIsScrollLocked:d}],get children(){return e.children}})}function k(){let t=e(D);return _(t,`undefined mobile context`),t}async function A(){if(document.title.split(` | `)[0]===`404`)return[];let e=document.querySelector(`meta[property="og:url"]`),t=e?.content?new URL(`index.json`,e.content).href:new URL(`index.json`,window.location.href).href;try{return(await(await fetch(t,{headers:{Accept:`application/json`}})).json()).sort((e,t)=>e.index<t.index?-1:1)}catch(e){return console.error(e),[]}}var j=s(`<div>Error`),M=document.getElementsByClassName(`container`)[0],N=a(async()=>await d(()=>import(`./fZjYgW.js`),__vite__mapDeps([0,1]))),P=a(async()=>await d(()=>import(`./zA1TQP.js`),__vite__mapDeps([2,1])));function F(e){return r(v,{get fallback(){return j()},get children(){return[r(o,{get when(){return e.isMobile},get children(){return r(O,{get children(){return r(P,{get closeText(){return e.closeText},get loadingText(){return e.loadingText}})}})}}),r(o,{get when(){return!e.isMobile},get children(){return r(T,{get children(){return r(N,{get prevText(){return e.prevText},get closeText(){return e.closeText},get nextText(){return e.nextText},get loadingText(){return e.loadingText}})}})}})]}})}function I(){let[e]=l(A),n=window.navigator.userAgent.toLowerCase(),i=`ontouchstart`in window||window.navigator.maxTouchPoints>0,a=window.matchMedia(`(pointer: coarse)`).matches||window.matchMedia(`(hover: none)`).matches,o=/android|iphone|ipad|ipod|mobile/.test(n),s=/windows nt/.test(n),c=o||i&&a&&!s;return r(t,{get when(){return e.state===`ready`},get children(){return r(g,{get images(){return e()??[]},get children(){return r(S,{get children(){return r(F,{isMobile:c,get prevText(){return M.dataset.prev},get closeText(){return M.dataset.close},get nextText(){return M.dataset.next},get loadingText(){return M.dataset.loading}})}})}})}})}m(()=>r(I,{}),M);export{E as n,C as r,k as t};
|
||||||
1
bundled/js/zA1TQP.js
Normal file
1
bundled/js/zA1TQP.js
Normal file
File diff suppressed because one or more lines are too long
32
docs.md
32
docs.md
@@ -15,6 +15,7 @@
|
|||||||
- [`outputs.toml`](#outputstoml)
|
- [`outputs.toml`](#outputstoml)
|
||||||
- [`params.toml`](#paramstoml)
|
- [`params.toml`](#paramstoml)
|
||||||
- [`sitemap.toml`](#sitemaptoml)
|
- [`sitemap.toml`](#sitemaptoml)
|
||||||
|
- [Usage](#usage)
|
||||||
- [Customizations](#customizations)
|
- [Customizations](#customizations)
|
||||||
- [Change Font](#change-font)
|
- [Change Font](#change-font)
|
||||||
- [Add a Custom Analytic Script](#add-a-custom-analytic-script)
|
- [Add a Custom Analytic Script](#add-a-custom-analytic-script)
|
||||||
@@ -25,24 +26,19 @@
|
|||||||
|
|
||||||
_[Contents](#contents)_
|
_[Contents](#contents)_
|
||||||
|
|
||||||
- Hugo (extended), minimum required version can be seen in the [`theme.toml`](https://github.com/Sped0n/bridget/blob/main/theme.toml#L19)
|
- [Hugo (extended)](https://gohugo.io/installation/), minimum required version can be seen in the [`theme.toml`](https://github.com/Sped0n/bridget/blob/main/theme.toml#L19)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
❯ hugo version
|
❯ hugo version
|
||||||
hugo v0.152.2+extended+withdeploy darwin/arm64 BuildDate=unknown VendorInfo=nixpkgs
|
hugo v0.152.2+extended+withdeploy darwin/arm64 BuildDate=unknown VendorInfo=nixpkgs
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Dart Sass](https://gohugo.io/functions/css/sass/#dart-sass) (**DO NOT INSTALL IT FROM NPM**, since it is doesn't support `--embedded`)
|
- [pnpm](https://pnpm.io/installation) and [Node.js](https://nodejs.org/en/download), please note that these two are only needed for customizations or development.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
❯ sass --embedded --version
|
❯ pnpm --version && node --version
|
||||||
{
|
10.20.0
|
||||||
"protocolVersion": "2.4.0",
|
v22.20.0
|
||||||
"compilerVersion": "1.70.0",
|
|
||||||
"implementationVersion": "1.70.0",
|
|
||||||
"implementationName": "dart-sass",
|
|
||||||
"id": 0
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -260,6 +256,14 @@ _[Contents](#contents)_
|
|||||||
|
|
||||||
https://gohugo.io/templates/sitemap-template/#configuration
|
https://gohugo.io/templates/sitemap-template/#configuration
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
Bridget will work as a normal Hugo theme (if you don't have needs to customize), https://gohugo.io/getting-started/usage/ is a great start.
|
||||||
|
|
||||||
|
For further reading, you can refer to the `scripts` field of `package.json`.
|
||||||
|
|
||||||
## Customizations
|
## Customizations
|
||||||
|
|
||||||
_[Contents](#contents)_
|
_[Contents](#contents)_
|
||||||
@@ -267,9 +271,11 @@ _[Contents](#contents)_
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Please make sure you have [installation with Git](#git-repository-for-customizations).
|
> Please make sure you have [installation with Git](#git-repository-for-customizations).
|
||||||
>
|
>
|
||||||
> - Use `pnpm install` to install neceessary dependencies.
|
> If you want to try some changes on the `exampleSite`, below are some commands you might need:
|
||||||
> - Use `pnpm run dev` to start a dev server (`http://localhost:1313`).
|
>
|
||||||
> - When you’re ready, run `pnpm run build` to update artifacts.
|
> - `pnpm install` to install dependencies.
|
||||||
|
> - `pnpm run dev` to start a dev server (`http://localhost:1313`).
|
||||||
|
> - `pnpm run build` to update artifacts.
|
||||||
|
|
||||||
### Change Font
|
### Change Font
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { default as eslint, default as js } from '@eslint/js'
|
import js from '@eslint/js'
|
||||||
import tsParser from '@typescript-eslint/parser'
|
import tsParser from '@typescript-eslint/parser'
|
||||||
import love from 'eslint-config-love'
|
import love from 'eslint-config-love'
|
||||||
import importPlugin from 'eslint-plugin-import'
|
import importPlugin from 'eslint-plugin-import'
|
||||||
@@ -9,11 +9,10 @@ import tseslint from 'typescript-eslint'
|
|||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
eslint.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
importPlugin.flatConfigs.recommended,
|
importPlugin.flatConfigs.recommended,
|
||||||
solid,
|
solid,
|
||||||
globalIgnores(['**/node_modules', '**/static', '**/exampleSite', '*.mjs']),
|
globalIgnores(['node_modules/', 'static/', 'exampleSite/', '*.mjs', 'bundled/']),
|
||||||
{
|
{
|
||||||
...love,
|
...love,
|
||||||
...prettier,
|
...prettier,
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ enableRobotsTXT = true
|
|||||||
[module]
|
[module]
|
||||||
replacements = "github.com/Sped0n/bridget/v2 -> ../.." # deploy with local dir (relative to hugo site theme dir) WARN: delete this line if you want to deploy with git
|
replacements = "github.com/Sped0n/bridget/v2 -> ../.." # deploy with local dir (relative to hugo site theme dir) WARN: delete this line if you want to deploy with git
|
||||||
[[module.imports]]
|
[[module.imports]]
|
||||||
path = "github.com/Sped0n/bridget/v2" # deploy with git (recommended) WARN: you should also set `bundled` to true in params.toml !!!
|
path = "github.com/Sped0n/bridget/v2" # deploy with git (recommended)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ build:
|
|||||||
publishResources: false
|
publishResources: false
|
||||||
---
|
---
|
||||||
|
|
||||||
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists, powered by <u>[Solid.js](https://www.solidjs.com)</u>.
|
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists, powered by <u>[SolidJS](https://www.solidjs.com)</u>.
|
||||||
|
|
||||||
The inspiration for this theme came from a video by <u>[Hyperlexed](https://www.youtube.com/@Hyperplexed)</u>, which can be found <u>[here](https://www.youtube.com/watch?v=Jt3A2lNN2aE)</u>. Initially, it was developed using no third-party dependencies. However, after website designer <u>[Tyler McRobert](https://tylermcrobert.com)</u> made the source code publicly available, I realized that I have invented many unnecessary wheels, and this project was modified to porting the original design to Hugo while focusing on _performance_.
|
The inspiration for this theme came from a video by <u>[Hyperlexed](https://www.youtube.com/@Hyperplexed)</u>, which can be found <u>[here](https://www.youtube.com/watch?v=Jt3A2lNN2aE)</u>. Initially, it was developed using no third-party dependencies. However, after website designer <u>[Tyler McRobert](https://tylermcrobert.com)</u> made the source code publicly available, I realized that I have invented many unnecessary wheels, and this project was modified to porting the original design to Hugo while focusing on _performance_.
|
||||||
|
|
||||||
|
|||||||
10
flake.lock
generated
10
flake.lock
generated
@@ -2,12 +2,12 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762596750,
|
"lastModified": 1769461804,
|
||||||
"narHash": "sha256-rXXuz51Bq7DHBlfIjN7jO8Bu3du5TV+3DSADBX7/9YQ=",
|
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||||
"rev": "b6a8526db03f735b89dd5ff348f53f752e7ddc8e",
|
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||||
"revCount": 891611,
|
"revCount": 935279,
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.891611%2Brev-b6a8526db03f735b89dd5ff348f53f752e7ddc8e/019a684c-ea63-75fd-99cc-3b869954e5f9/source.tar.gz"
|
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.935279%2Brev-bfc1b8a4574108ceef22f02bafcf6611380c100d/019c02ef-f13d-717e-8527-f1603ec205db/source.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
nodejs
|
nodejs
|
||||||
nodePackages.pnpm
|
nodePackages.pnpm
|
||||||
dart-sass
|
|
||||||
hugo
|
hugo
|
||||||
go
|
go
|
||||||
];
|
];
|
||||||
|
|||||||
18
hugo.toml
Normal file
18
hugo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[[module.mounts]]
|
||||||
|
source = 'archetypes'
|
||||||
|
target = 'archetypes'
|
||||||
|
[[module.mounts]]
|
||||||
|
source = 'assets'
|
||||||
|
target = 'assets'
|
||||||
|
[[module.mounts]]
|
||||||
|
source = 'layouts'
|
||||||
|
target = 'layouts'
|
||||||
|
[[module.mounts]]
|
||||||
|
source = 'static'
|
||||||
|
target = 'static'
|
||||||
|
[[module.mounts]]
|
||||||
|
source = "bundled"
|
||||||
|
target = "assets/bundled"
|
||||||
|
[[module.mounts]]
|
||||||
|
source = "bundled"
|
||||||
|
target = "static/bundled"
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
{{- $fingerprint := .Scratch.Get "fingerprint" | default "" -}}
|
{{- $fingerprint := .Scratch.Get "fingerprint" | default "" -}}
|
||||||
|
|
||||||
{{- /* critical style */ -}}
|
{{- /* critical style */ -}}
|
||||||
{{- $style := dict "Source" "scss/critical.scss" "Fingerprint" $fingerprint -}}
|
{{- $style := dict "Source" "bundled/css/critical.css" "Fingerprint" $fingerprint -}}
|
||||||
{{- $options := dict "enableSourceMap" true "includePaths" (slice "node_modules") "transpiler" "dartsass" -}}
|
{{- $options := dict "enableSourceMap" false -}}
|
||||||
{{- $style = dict "Context" . "ToCSS" $options "Inline" true "Template" true | merge $style -}}
|
{{- $style = dict "Context" . "ToCSS" $options "Inline" true "Template" true | merge $style -}}
|
||||||
{{- partial "plugin/style.html" $style -}}
|
{{- partial "plugin/style.html" $style -}}
|
||||||
|
|
||||||
|
|||||||
30
package.json
30
package.json
@@ -3,7 +3,7 @@
|
|||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "bridget theme source file",
|
"description": "bridget theme source file",
|
||||||
"packageManager": "pnpm@8.10.2",
|
"packageManager": "pnpm@10.20.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -39,29 +39,31 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/Sped0n/bridget#readme",
|
"homepage": "https://github.com/Sped0n/bridget#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.0",
|
"@eslint/js": "^9.39.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
"@types/node": "^25.5.0",
|
||||||
"@typescript-eslint/parser": "^8.46.4",
|
"@typescript-eslint/parser": "^8.57.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-love": "^133.0.0",
|
"eslint-config-love": "^151.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint-plugin-solid": "^0.14.5",
|
"eslint-plugin-solid": "^0.14.5",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.8.1",
|
||||||
"prettier-plugin-go-template": "^0.0.15",
|
"prettier-plugin-go-template": "^0.0.15",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"sass": "^1.94.0",
|
"sass-embedded": "^1.98.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.2",
|
"typescript-eslint": "^8.53.1",
|
||||||
"vite-plugin-solid": "^2.11.10"
|
"vite": "^8.0.1",
|
||||||
|
"vite-plugin-solid": "^2.11.11",
|
||||||
|
"vitefu": "^1.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.14.2",
|
||||||
"solid-js": "^1.9.10",
|
"solid-js": "^1.9.11",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.1.2",
|
||||||
"tiny-invariant": "^1.3.3"
|
"tiny-invariant": "^1.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5958
pnpm-lock.yaml
generated
5958
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
22
vercel.sh
22
vercel.sh
@@ -1,22 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen"
|
node_modules_generated_dir="./node_modules/exampleSite/resources/_gen/images"
|
||||||
project_generated_dir="./exampleSite/resources/_gen"
|
project_generated_dir="./exampleSite/resources/_gen/images"
|
||||||
dart_sass_version="1.93.3"
|
|
||||||
dart_sass_install_dir="${HOME}/.local/dart-sass"
|
|
||||||
dart_sass_tarball="dart-sass-${dart_sass_version}-linux-x64.tar.gz"
|
|
||||||
dart_sass_download_url="https://github.com/sass/dart-sass/releases/download/${dart_sass_version}/${dart_sass_tarball}"
|
|
||||||
|
|
||||||
install_dart_sass() {
|
|
||||||
echo "Installing Dart Sass ${dart_sass_version}..."
|
|
||||||
mkdir -p "${HOME}/.local"
|
|
||||||
curl -sSLO "${dart_sass_download_url}"
|
|
||||||
rm -rf "${dart_sass_install_dir}"
|
|
||||||
tar -C "${HOME}/.local" -xf "${dart_sass_tarball}"
|
|
||||||
rm -f "${dart_sass_tarball}"
|
|
||||||
export PATH="${dart_sass_install_dir}:${PATH}"
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_generated_assets_to_project() {
|
copy_generated_assets_to_project() {
|
||||||
if [ -d "${node_modules_generated_dir}" ]; then
|
if [ -d "${node_modules_generated_dir}" ]; then
|
||||||
@@ -30,7 +16,8 @@ copy_generated_assets_to_project() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_site_build() {
|
run_site_build() {
|
||||||
pnpm run vite:build && hugo --logLevel info --source=exampleSite --gc --minify
|
rm -rf bundled
|
||||||
|
pnpm run build
|
||||||
}
|
}
|
||||||
|
|
||||||
copy_generated_assets_to_node_modules() {
|
copy_generated_assets_to_node_modules() {
|
||||||
@@ -44,7 +31,6 @@ copy_generated_assets_to_node_modules() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_dart_sass
|
|
||||||
copy_generated_assets_to_project
|
copy_generated_assets_to_project
|
||||||
run_site_build
|
run_site_build
|
||||||
copy_generated_assets_to_node_modules
|
copy_generated_assets_to_node_modules
|
||||||
|
|||||||
@@ -4,20 +4,30 @@ import solidPlugin from 'vite-plugin-solid'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solidPlugin()],
|
plugins: [solidPlugin()],
|
||||||
build: {
|
build: {
|
||||||
outDir: './static/bundled',
|
outDir: './bundled',
|
||||||
|
cssMinify: 'esbuild',
|
||||||
watch: process.env.DISABLE_WATCH
|
watch: process.env.DISABLE_WATCH
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
include: 'assets/**'
|
include: 'assets/**'
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: './assets/ts/main.tsx',
|
input: {
|
||||||
|
main: './assets/ts/main.tsx',
|
||||||
|
critical: './assets/ts/critical.ts'
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
format: 'es',
|
format: 'es',
|
||||||
entryFileNames: 'js/[name].js',
|
entryFileNames: 'js/[name].js',
|
||||||
chunkFileNames: 'js/[hash:6].js',
|
chunkFileNames: 'js/[hash:6].js',
|
||||||
assetFileNames: '[ext]/[name].[ext]',
|
assetFileNames: '[ext]/[name].[ext]'
|
||||||
compact: true
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
loadPaths: ['./assets/scss']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user