mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-17 03:29:31 -07:00
Compare commits
280 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 | ||
|
|
ba46f52d55 | ||
|
|
48eaa9d95b | ||
|
|
6be1a924de | ||
|
|
ba0def753e | ||
|
|
c9410c9644 | ||
|
|
1d1f893c92 | ||
|
|
7d4bed3ba6 | ||
|
|
72b830def9 | ||
|
|
6b2c1de9d8 | ||
|
|
ae899035ae | ||
|
|
36721f8bf6 | ||
|
|
2dcbb5e5d5 | ||
|
|
07aa48f2db | ||
|
|
099513500c | ||
|
|
f0da771dca | ||
|
|
dfd4abdf2a | ||
|
|
6f8ce6bbd8 | ||
|
|
543a08d472 | ||
|
|
05599ef190 | ||
|
|
95bd4d1c28 | ||
|
|
835cd6f343 | ||
|
|
479164bc83 | ||
|
|
9ea37b8a3f | ||
|
|
5df519e55f | ||
|
|
860428d03b | ||
|
|
c1ad92fbeb | ||
|
|
78f657618e | ||
|
|
1fe7095898 | ||
|
|
0471af5085 | ||
|
|
543d630535 | ||
|
|
b7ed5a2834 | ||
|
|
5ba7d77c07 | ||
|
|
af82026d1a | ||
|
|
b5ef661e1c | ||
|
|
883eec1a3b | ||
|
|
b027cd03cf | ||
|
|
aa34979bb4 | ||
|
|
08bbfaa3ba | ||
|
|
b8c6ce2b04 | ||
|
|
27083c0336 | ||
|
|
082458b2aa | ||
|
|
1c05eb2633 | ||
|
|
9016cfb035 | ||
|
|
d9b4100d17 | ||
|
|
082f5f4961 | ||
|
|
9d91becbd5 | ||
|
|
2e51b7eb89 | ||
|
|
7dd8c2242c | ||
|
|
2a10e4944e | ||
|
|
7893586d24 | ||
|
|
5a51f83654 | ||
|
|
e4d5ac4389 | ||
|
|
ea3d58760b | ||
|
|
4812cdb191 | ||
|
|
b93b8d3ac6 | ||
|
|
a7bc6b2df5 | ||
|
|
1cfbc8ac28 | ||
|
|
c4cea2648e | ||
|
|
26bdddc5ff | ||
|
|
437bbf17e0 | ||
|
|
2a715327f6 | ||
|
|
c111de15b1 | ||
|
|
1b9826f582 | ||
|
|
6a3ce498a9 | ||
|
|
8d48e6347e | ||
|
|
4599a5dfc2 | ||
|
|
7536288baa | ||
|
|
e12c32388b | ||
|
|
4198a5fa90 | ||
|
|
089e9b285a | ||
|
|
60e19fed00 | ||
|
|
626433e67d | ||
|
|
cb5080ce41 | ||
|
|
e2f8317669 | ||
|
|
5d9e32f62b | ||
|
|
b96ecd6042 | ||
|
|
5b7ec62106 | ||
|
|
3ca4a0d803 | ||
|
|
a8d8802d9f | ||
|
|
28782217f1 | ||
|
|
cfcda29524 | ||
|
|
fb498971c7 | ||
|
|
9dbb3cb624 | ||
|
|
129f26dd54 | ||
|
|
d1f9b843c3 | ||
|
|
a7b5ec45ed | ||
|
|
5d82276734 | ||
|
|
b0c4fa8ea7 | ||
|
|
a6f983de5d | ||
|
|
3bc232638a | ||
|
|
67944df12f | ||
|
|
e82fe6cab2 | ||
|
|
819df6b2ed | ||
|
|
dfef87ca55 | ||
|
|
477b6d748a | ||
|
|
31a59c5e9e | ||
|
|
e8cdd12151 | ||
|
|
a8bc17ca12 | ||
|
|
4d04fe1945 | ||
|
|
bafd2aa3b3 | ||
|
|
acf50d10d7 | ||
|
|
30a6a3bd23 | ||
|
|
d808782afd | ||
|
|
5327d7c585 | ||
|
|
d7e7fc68ba | ||
|
|
7b637637a1 | ||
|
|
4a2ef258ba | ||
|
|
b2c1c96f56 | ||
|
|
6d98671856 | ||
|
|
0b7605b3ff | ||
|
|
c9ca9c682a | ||
|
|
417a00b9c4 | ||
|
|
d7a4345f53 | ||
|
|
2e47be5635 | ||
|
|
0e6fc9384f | ||
|
|
81eba485d4 | ||
|
|
b1553a9a8b | ||
|
|
66735f4c65 | ||
|
|
3ff49b1106 | ||
|
|
633f6a40e2 | ||
|
|
1ff94546e0 | ||
|
|
e081e139fc | ||
|
|
875113448b | ||
|
|
53a44776de | ||
|
|
ecdaebb6cd | ||
|
|
1a02360214 | ||
|
|
febbd7d45d | ||
|
|
1acf24a519 | ||
|
|
e2afe91131 | ||
|
|
3e51b96825 | ||
|
|
c84b4cf234 | ||
|
|
997207fa90 | ||
|
|
f7d2c7962c | ||
|
|
0812a5a6b8 | ||
|
|
7fd971eb13 | ||
|
|
881b0b6490 | ||
|
|
50d7b14133 | ||
|
|
872d23ad13 | ||
|
|
eeca83a74b | ||
|
|
3170f5b65a | ||
|
|
bf1c5e21bc | ||
|
|
d08e2c92b8 | ||
|
|
ba07636f8f | ||
|
|
a98c6a4a60 | ||
|
|
c1414bbfc5 |
@@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
static
|
|
||||||
exampleSite
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"standard-with-typescript",
|
|
||||||
"prettier",
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"overrides": [],
|
|
||||||
"plugins": ["prettier", "@typescript-eslint"],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"project": "./tsconfig.json",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"arrow-body-style": "off",
|
|
||||||
"prefer-arrow-callback": "off",
|
|
||||||
"import/no-cycle": "error",
|
|
||||||
"@typescript-eslint/non-nullable-type-assertion-style": "off",
|
|
||||||
"sort-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"ignoreCase": false,
|
|
||||||
"ignoreDeclarationSort": true,
|
|
||||||
"ignoreMemberSort": false,
|
|
||||||
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
|
|
||||||
"allowSeparatedGroups": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"import/no-unresolved": "error",
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
|
|
||||||
"newlines-between": "always",
|
|
||||||
"alphabetize": {
|
|
||||||
"order": "asc",
|
|
||||||
"caseInsensitive": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"typescript": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Environment (please complete the following information):**
|
|
||||||
|
|
||||||
- OS: [e.g. macOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Hugo Version [e.g. v0.114.0 extended]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
115
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
115
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Create a bug report
|
||||||
|
labels:
|
||||||
|
- 'T: Bug'
|
||||||
|
- 'S: Untriaged'
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please finish verify steps which list in the end first before create bug report
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: Step to reproduce
|
||||||
|
description: |
|
||||||
|
Please write down the reproduction steps here and include the error log. If necessary, please provide screenshots or recordings.
|
||||||
|
placeholder: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
[Screen recording]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behaviour
|
||||||
|
description: |
|
||||||
|
Describe what should happened here
|
||||||
|
placeholder: |
|
||||||
|
It should be ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual Behaviour
|
||||||
|
description: |
|
||||||
|
Describe what actually happened here, screenshots is better
|
||||||
|
placeholder: |
|
||||||
|
Actually it ...
|
||||||
|
[Screenshots]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: env
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: |
|
||||||
|
CLI output or browser log.
|
||||||
|
placeholder: |
|
||||||
|
❯ pnpm run server
|
||||||
|
|
||||||
|
> bridget@v1.0.0 server /Users/spedon/eden/hugo/bridget
|
||||||
|
> run-p vite:server hugo:server
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: more
|
||||||
|
attributes:
|
||||||
|
label: Addition details
|
||||||
|
description: |
|
||||||
|
Describe addition details here
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: hugo-version
|
||||||
|
attributes:
|
||||||
|
label: Hugo version
|
||||||
|
description: You can get version output with hugo --version
|
||||||
|
placeholder: v0.114.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: bridget-version
|
||||||
|
attributes:
|
||||||
|
label: Bridget version
|
||||||
|
description: Release version or commit SHA
|
||||||
|
placeholder: v1.0.1 or 633f6a40e202a023471c58c09f05a92ec2130c93
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: system
|
||||||
|
attributes:
|
||||||
|
label: OS version
|
||||||
|
description: OS + version code
|
||||||
|
placeholder: Windows 11, macOS 14
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: check
|
||||||
|
attributes:
|
||||||
|
label: Verify steps
|
||||||
|
description: |
|
||||||
|
Please ensure you have obtained all needed options
|
||||||
|
options:
|
||||||
|
- label: Pull request is welcome. Check this if you want to start a pull request
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- label: I have searched on [Issue Tracker](https://github.com/Sped0n/bridget/issues), No duplicate or related open issue has been found
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- label: Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
||||||
|
required: true
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: All other questions
|
||||||
|
url: https://github.com/Sped0n/bridget/discussions
|
||||||
|
about: Turn to discussions
|
||||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
60
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea
|
||||||
|
labels:
|
||||||
|
- 'T: Feature'
|
||||||
|
- 'S: Untriaged'
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please finish verify steps which list in the end first before suggest an idea
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: request
|
||||||
|
attributes:
|
||||||
|
label: Requirement
|
||||||
|
description: |
|
||||||
|
Ddescribe what you need here.
|
||||||
|
placeholder: |
|
||||||
|
I want ABC feature ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: impl
|
||||||
|
attributes:
|
||||||
|
label: Suggested implements
|
||||||
|
description: |
|
||||||
|
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
|
||||||
|
placeholder: |
|
||||||
|
I recommend add ABC feature to DEF...
|
||||||
|
Photos (if exists)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: more
|
||||||
|
attributes:
|
||||||
|
label: Addition details
|
||||||
|
description: |
|
||||||
|
Describe addition details here
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: check
|
||||||
|
attributes:
|
||||||
|
label: Verify steps
|
||||||
|
description: |
|
||||||
|
Please ensure you have obtained all needed options
|
||||||
|
options:
|
||||||
|
- label: Pull request is welcome. Check this if you want to start a pull request
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- label: I have searched on [Issue Tracker](https://github.com/Sped0n/bridget/issues), No duplicate or related open issue has been found
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- label: Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
|
||||||
|
required: true
|
||||||
134
.github/workflows/build.yml
vendored
134
.github/workflows/build.yml
vendored
@@ -12,82 +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@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get changed files in scope
|
|
||||||
id: changed-files-specific
|
|
||||||
uses: tj-actions/changed-files@v41
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
package.json
|
|
||||||
assets/**
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build
|
name: Build (Hugo ${{ matrix.hugo-label }})
|
||||||
needs: [filter]
|
if: github.event.repository.fork == false
|
||||||
if: |
|
strategy:
|
||||||
github.ref == 'refs/heads/main' &&
|
matrix:
|
||||||
github.event.repository.fork == false
|
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@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Hugo
|
|
||||||
uses: peaceiris/actions-hugo@v2
|
|
||||||
with:
|
|
||||||
hugo-version: '0.114.0'
|
|
||||||
extended: true
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
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@v3
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: ${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Setup hugo cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ./exampleSite/resources
|
|
||||||
key: ${{ runner.os }}-hugo-${{ hashFiles('./exampleSite') }}
|
|
||||||
restore-keys: ${{ runner.os }}-hugo-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm run build
|
|
||||||
|
|
||||||
- name: Push artifacts
|
|
||||||
if: ${{ (github.event_name == 'push' || github.event.pull_request.merged == true) && needs.filter.outputs.any_changed == 'true' }}
|
|
||||||
uses: peter-evans/create-pull-request@v5
|
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PAT }}
|
token: ${{ secrets.PAT }}
|
||||||
title: "ci: update bundled artifacts"
|
|
||||||
commit-message: "ci: update bundled artifacts"
|
- name: Setup Mise
|
||||||
branch: update-artifacts-${{ steps.version.outputs.builddate }}
|
uses: jdx/mise-action@v3
|
||||||
base: main
|
with:
|
||||||
|
install_args: node@latest pnpm@10 hugo-extended@${{ matrix.hugo-version }}
|
||||||
|
tool_versions: |
|
||||||
|
node latest
|
||||||
|
pnpm 10
|
||||||
|
hugo-extended ${{ matrix.hugo-version }}
|
||||||
|
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: Setup Hugo cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: exampleSite/resources/_gen
|
||||||
|
key: hugo-${{ matrix.hugo-version }}-${{ hashFiles('./exampleSite/**/*.jpg') }}
|
||||||
|
restore-keys: |
|
||||||
|
hugo-${{ matrix.hugo-version }}-
|
||||||
|
|
||||||
|
- name: Install project dependencies
|
||||||
|
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
|
||||||
|
run: 'pnpm run build'
|
||||||
|
|
||||||
|
- name: Push artifacts
|
||||||
|
if: >
|
||||||
|
matrix.hugo-version == 'latest' &&
|
||||||
|
(github.event_name == 'push' || github.event.pull_request.merged == true)
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
|
with:
|
||||||
|
file_pattern: 'bundled/**/*.js bundled/**/*.css'
|
||||||
|
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}}'
|
||||||
|
|||||||
42
.github/workflows/eslint.yml
vendored
42
.github/workflows/eslint.yml
vendored
@@ -1,42 +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@v3
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
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@v3
|
|
||||||
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
|
|
||||||
run: pnpm run lint:check
|
|
||||||
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,6 +1,6 @@
|
|||||||
node_modules
|
node_modules/
|
||||||
static
|
static/
|
||||||
exmapleSite
|
exmapleSite/
|
||||||
*.yaml
|
|
||||||
*.yml
|
|
||||||
single.json
|
single.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
bundled/
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"printWidth": 88,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"semi": false,
|
|
||||||
"plugins": ["prettier-plugin-go-template", "prettier-plugin-organize-imports"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.html"],
|
|
||||||
"options": {
|
|
||||||
"parser": "go-template"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
hi@sped0nwen.com.
|
hi@sped0n.com.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -2,47 +2,41 @@
|
|||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
Bridget is a minimal [Hugo](https://gohugo.io) theme designed for photographers / visual artists.
|
Bridget is a minimal [Hugo](https://gohugo.io) theme for photographers/visual artists, based on https://github.com/tylermcrobert/bridget-pictures-www.
|
||||||
|
|
||||||
It’s based on the https://github.com/tylermcrobert/bridget-pictures-www.
|
Here is a [live demo](https://bridget-demo.sped0n.com).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## [Demo Site](https://bridget-demo.sped0nwen.com)
|
> [!NOTE]
|
||||||
|
> This repository is currently in **maintaince mode** for two reasons:
|
||||||
To see this theme in action, here is a live [demo site](https://bridget-demo.sped0nwen.com) which is rendered with **Bridget** theme.
|
>
|
||||||
|
> 1. I want to keep this theme minimal.
|
||||||
|
> 2. My bandwith after work is limited.
|
||||||
|
>
|
||||||
|
> BUT, bug fixes will be addressed (including related issues and PRs), and they are the **number one priority** for this project.
|
||||||
|
>
|
||||||
|
> Please understand that feature request might **NOT** be addressed or may take a long time to be implemented.
|
||||||
|
>
|
||||||
|
> Anyway, forks are welcomed, and I'm looking forward to seeing what you can do with the theme.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Head to this [documentation](https://github.com/Sped0n/bridget/blob/main/doc/getStarted.md) for a complete guidance to get started with the Bridget theme.
|
Head to this [documentation](https://github.com/Sped0n/bridget/blob/main/docs.md) for a complete guidance to get started with the theme.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Blazingly fast**: 99/100 on mobile and 100/100 on desktop in [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights)
|
- **Blazingly fast**: 100/100 on both desktop and mobile in [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights)
|
||||||
|
- Powered by **[SolidJS](https://www.solidjs.com)**, a declarative, efficient, and flexible JavaScript library for building user interfaces
|
||||||
- JS **dynamic loading** (powered by ES6 syntax)
|
- JS **dynamic loading**
|
||||||
- JS **code splitting** by [rollup.js](https://rollupjs.org)
|
- Image **preloading** + **lazy loading**
|
||||||
- Image **Preloading**/**Lazy loading**
|
|
||||||
- **Dynamic resolution** based on view mode
|
- **Dynamic resolution** based on view mode
|
||||||
- Multiple **analytics** services supported
|
- Multiple **analytics** services supported
|
||||||
- Search engine **verification** supported (Google, Bind, Yandex and Baidu)
|
- Search engine **verification** supported (Google, Bind, Yandex and Baidu)
|
||||||
|
|
||||||
## Multilingual and i18n
|
|
||||||
|
|
||||||
Bridget supports the following languages:
|
|
||||||
|
|
||||||
- English
|
|
||||||
- Simplified Chinese
|
|
||||||
- Traditional Chinese
|
|
||||||
- Japanese
|
|
||||||
- Korean
|
|
||||||
- Deutsch
|
|
||||||
- Spanish
|
|
||||||
- Italian
|
|
||||||
- [Contribute with a new language](https://github.com/Sped0n/bridget/pulls)
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- https://github.com/tylermcrobert/bridget-pictures-www
|
- https://github.com/tylermcrobert/bridget-pictures-www
|
||||||
- https://www.youtube.com/watch?v=Jt3A2lNN2aE
|
- https://www.youtube.com/watch?v=Jt3A2lNN2aE
|
||||||
- https://github.com/d4cho/bridget-pictures-clone
|
- https://github.com/d4cho/bridget-pictures-clone
|
||||||
|
- https://www.solidjs.com/tutorial
|
||||||
|
|||||||
@@ -17,7 +17,3 @@ a,
|
|||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,48 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Geist';
|
font-family: 'Geist';
|
||||||
src:
|
src:
|
||||||
url('/lib/fonts/GeistVF.woff2') format('woff2 supports variations'),
|
url(/* @vite-ignore */'{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||||
url('/lib/fonts/GeistVF.woff2') format('woff2-variations');
|
format('woff2 supports variations'),
|
||||||
font-weight: 400;
|
url(/* @vite-ignore */'{{- "lib/fonts/GeistVF.woff2" | absURL -}}')
|
||||||
font-style: normal;
|
format('woff2-variations');
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans CJK SC';
|
|
||||||
src: url('/lib/fonts/NotoSansCJKsc-Regular.woff2') format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans CJK TC';
|
|
||||||
src: url('/lib/fonts/NotoSansCJKtc-Regular.woff2') format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans CJK JP';
|
|
||||||
src: url('/lib/fonts/NotoSansCJKjp-Regular.woff2') format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans CJK KR';
|
|
||||||
src: url('/lib/fonts/NotoSansCJKkr-Regular.woff2') format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans';
|
|
||||||
src: url('/lib/fonts/NotoSans-Regular.woff2') format('woff2');
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -50,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
assets/scss/_core/_foundation.scss
Normal file
4
assets/scss/_core/_foundation.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@forward 'reset';
|
||||||
|
@forward 'font';
|
||||||
|
@forward 'typography';
|
||||||
|
@forward 'mixins';
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use 'sass:map';
|
||||||
|
|
||||||
$breakpoints: (
|
$breakpoints: (
|
||||||
'mobile': 375px,
|
'mobile': 375px,
|
||||||
'tablet': 768px,
|
'tablet': 768px,
|
||||||
@@ -8,8 +10,8 @@ $breakpoints: (
|
|||||||
// Breakpoints
|
// Breakpoints
|
||||||
|
|
||||||
@mixin min-width($breakpoint) {
|
@mixin min-width($breakpoint) {
|
||||||
@if map-has-key($breakpoints, $breakpoint) {
|
@if map.has-key($breakpoints, $breakpoint) {
|
||||||
@media (min-width: map-get($breakpoints, $breakpoint)) {
|
@media (min-width: map.get($breakpoints, $breakpoint)) {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
@@ -18,8 +20,8 @@ $breakpoints: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin max-width($breakpoint) {
|
@mixin max-width($breakpoint) {
|
||||||
@if map-has-key($breakpoints, $breakpoint) {
|
@if map.has-key($breakpoints, $breakpoint) {
|
||||||
@media (max-width: (map-get($breakpoints, $breakpoint) - 1px)) {
|
@media (max-width: (map.get($breakpoints, $breakpoint) - 1px)) {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
@import 'mixins';
|
@use 'mixins' as *;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: sans-serif;
|
font-family: 'Geist', sans-serif;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-family: 'FW';
|
font-family: 'FW', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include min-width('tablet') {
|
@include min-width('tablet') {
|
||||||
@@ -16,51 +16,3 @@ body {
|
|||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body:lang(en) {
|
|
||||||
font-family: 'Geist', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(de) {
|
|
||||||
font-family: 'Geist', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(es) {
|
|
||||||
font-family: 'Geist', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(fr) {
|
|
||||||
font-family: 'Geist', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(it) {
|
|
||||||
font-family: 'Geist', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(zh-cn) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK SC', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(zh-sg) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK SC', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(zh-hk) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK TC', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(zh-mo) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK TC', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(zh-tw) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK TC', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(ja) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK JP', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:lang(ko) {
|
|
||||||
font-family: 'Noto Sans', 'Noto Sans CJK KR', sans-serif;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
.info {
|
@use 'sass:map';
|
||||||
|
|
||||||
|
@use '_core/mixins' as *;
|
||||||
|
|
||||||
|
$tablet: map.get($breakpoints, 'tablet') - 1;
|
||||||
|
|
||||||
|
article {
|
||||||
padding: var(--space-standard);
|
padding: var(--space-standard);
|
||||||
max-width: 25em;
|
max-width: 25em;
|
||||||
|
|
||||||
@@ -42,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $tablet), (hover: none) {
|
@media (max-width: $tablet), (hover: none) {
|
||||||
.info {
|
article {
|
||||||
margin-top: var(--nav-height);
|
margin-top: var(--nav-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20vh;
|
gap: 20vh;
|
||||||
|
|
||||||
padding-top: 50vh;
|
padding-top: calc(var(--window-height) * 0.4);
|
||||||
margin-top: calc(var(--nav-height) * -1);
|
margin-top: calc(var(--nav-height) * -1);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 50vh;
|
top: calc(var(--window-height) * 0.4);
|
||||||
|
|
||||||
width: 60vw;
|
width: 60vw;
|
||||||
height: 20vh;
|
height: 20vh;
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 20vh;
|
margin-bottom: calc(var(--window-height) * 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
$tablet: map-get($breakpoints, 'tablet') - 1;
|
@use 'sass:map';
|
||||||
|
|
||||||
|
@use '_core/mixins' as *;
|
||||||
|
|
||||||
|
$tablet: map.get($breakpoints, 'tablet') - 1;
|
||||||
|
|
||||||
@media (max-width: $tablet), (hover: none) {
|
@media (max-width: $tablet), (hover: none) {
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
$tablet: map-get($breakpoints, 'tablet') - 1;
|
@use 'sass:map';
|
||||||
|
|
||||||
|
@use '_core/mixins' as *;
|
||||||
|
|
||||||
|
$tablet: map.get($breakpoints, 'tablet') - 1;
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import '_core/mixins';
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--window-height: 100vh;
|
--window-height: 100vh;
|
||||||
--nav-height: 2rem;
|
--nav-height: 2rem;
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
@charset "utf-8";
|
@charset "utf-8";
|
||||||
|
|
||||||
@import '_core/reset';
|
@use '_core/foundation';
|
||||||
@import '_core/font';
|
@use '_variables';
|
||||||
@import '_core/typography';
|
@use '_core/base';
|
||||||
@import '_core/mixins';
|
|
||||||
@import '_variables';
|
|
||||||
@import '_core/base';
|
|
||||||
|
|
||||||
@import '_partial/nav';
|
@use '_partial/nav';
|
||||||
@import '_partial/article';
|
@use '_partial/article';
|
||||||
@import '_partial/container';
|
@use '_partial/container';
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
@charset "utf-8";
|
@charset "utf-8";
|
||||||
|
|
||||||
@import '_partial/customCursor';
|
@use '_partial/customCursor';
|
||||||
@import '_partial/stage';
|
@use '_partial/stage';
|
||||||
@import '_partial/stageNav';
|
@use '_partial/stageNav';
|
||||||
|
@use '_partial/collection';
|
||||||
|
@use '_partial/gallery';
|
||||||
|
|
||||||
@import '_partial/collection';
|
@use '../../node_modules/swiper/swiper.css';
|
||||||
@import '_partial/gallery';
|
|
||||||
|
|
||||||
@import 'node_modules/swiper/swiper.scss';
|
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { Watchable } from './globalUtils'
|
|
||||||
|
|
||||||
export const scrollable = new Watchable<boolean>(true)
|
|
||||||
|
|
||||||
export let container: Container
|
|
||||||
|
|
||||||
/**
|
|
||||||
* interfaces
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Container extends HTMLDivElement {
|
|
||||||
dataset: {
|
|
||||||
next: string
|
|
||||||
close: string
|
|
||||||
prev: string
|
|
||||||
loading: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initContainer(): void {
|
|
||||||
container = document.getElementsByClassName('container').item(0) as Container
|
|
||||||
scrollable.addWatcher((o) => {
|
|
||||||
if (o) {
|
|
||||||
container.classList.remove('disableScroll')
|
|
||||||
} else {
|
|
||||||
container.classList.add('disableScroll')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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'
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { container } from '../container'
|
|
||||||
|
|
||||||
import { active } from './state'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
const cursor = document.createElement('div')
|
|
||||||
const cursorInner = document.createElement('div')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onMouse(e: MouseEvent): void {
|
|
||||||
const x = e.clientX
|
|
||||||
const y = e.clientY
|
|
||||||
cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCustomCursor(text: string): void {
|
|
||||||
cursorInner.innerText = text
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init
|
|
||||||
*/
|
|
||||||
export function initCustomCursor(): void {
|
|
||||||
// cursor class name
|
|
||||||
cursor.className = 'cursor'
|
|
||||||
// cursor inner class name
|
|
||||||
cursorInner.className = 'cursorInner'
|
|
||||||
// append cursor inner to cursor
|
|
||||||
cursor.append(cursorInner)
|
|
||||||
// append cursor to main
|
|
||||||
container.append(cursor)
|
|
||||||
// bind mousemove event to window
|
|
||||||
window.addEventListener('mousemove', onMouse, { passive: true })
|
|
||||||
// add active callback
|
|
||||||
active.addWatcher((o) => {
|
|
||||||
if (o) {
|
|
||||||
cursor.classList.add('active')
|
|
||||||
} else {
|
|
||||||
cursor.classList.remove('active')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
51
assets/ts/desktop/customCursor.tsx
Normal file
51
assets/ts/desktop/customCursor.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createSignal, onCleanup, onMount, type Accessor, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
export default function CustomCursor(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
active: Accessor<boolean>
|
||||||
|
cursorText: Accessor<string>
|
||||||
|
}): JSX.Element {
|
||||||
|
// types
|
||||||
|
interface XY {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// variables
|
||||||
|
let controller: AbortController | undefined
|
||||||
|
|
||||||
|
// states
|
||||||
|
const [xy, setXy] = createSignal<XY>({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
const onMouse: (e: MouseEvent) => void = (e) => {
|
||||||
|
const { clientX, clientY } = e
|
||||||
|
setXy({ x: clientX, y: clientY })
|
||||||
|
}
|
||||||
|
|
||||||
|
// effects
|
||||||
|
onMount(() => {
|
||||||
|
controller = new AbortController()
|
||||||
|
const abortSignal = controller.signal
|
||||||
|
window.addEventListener('mousemove', onMouse, {
|
||||||
|
passive: true,
|
||||||
|
signal: abortSignal
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
controller?.abort()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
class="cursor"
|
||||||
|
classList={{ active: props.active() }}
|
||||||
|
style={{ transform: `translate3d(${xy().x}px, ${xy().y}px, 0)` }}
|
||||||
|
>
|
||||||
|
<div class="cursorInner">{props.cursorText()}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { type ImageJSON } from '../resources'
|
|
||||||
|
|
||||||
import { initCustomCursor } from './customCursor'
|
|
||||||
import { initStage } from './stage'
|
|
||||||
import { initStageNav } from './stageNav'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initDesktop(ijs: ImageJSON[]): void {
|
|
||||||
initCustomCursor()
|
|
||||||
initStage(ijs)
|
|
||||||
initStageNav()
|
|
||||||
}
|
|
||||||
61
assets/ts/desktop/layout.tsx
Normal file
61
assets/ts/desktop/layout.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Show, createMemo, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
|
||||||
|
import CustomCursor from './customCursor'
|
||||||
|
import Nav from './nav'
|
||||||
|
import Stage from './stage'
|
||||||
|
import StageNav from './stageNav'
|
||||||
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* interfaces and types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DesktopImage extends HTMLImageElement {
|
||||||
|
dataset: {
|
||||||
|
hiUrl: string
|
||||||
|
hiImgH: string
|
||||||
|
hiImgW: string
|
||||||
|
loUrl: string
|
||||||
|
loImgH: string
|
||||||
|
loImgW: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* components
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function Desktop(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
prevText: string
|
||||||
|
closeText: string
|
||||||
|
nextText: string
|
||||||
|
loadingText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
const imageState = useImageState()
|
||||||
|
const [desktop] = useDesktopState()
|
||||||
|
|
||||||
|
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||||
|
const cursorText = createMemo(() =>
|
||||||
|
desktop.isLoading() ? props.loadingText : desktop.hoverText()
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Nav />
|
||||||
|
<Show when={imageState().length > 0}>
|
||||||
|
<Stage />
|
||||||
|
<Show when={desktop.isOpen()}>
|
||||||
|
<CustomCursor cursorText={cursorText} active={active} />
|
||||||
|
<StageNav
|
||||||
|
prevText={props.prevText}
|
||||||
|
closeText={props.closeText}
|
||||||
|
nextText={props.nextText}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
assets/ts/desktop/nav.tsx
Normal file
68
assets/ts/desktop/nav.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { createEffect, onCleanup, onMount } from 'solid-js'
|
||||||
|
|
||||||
|
import { useConfigState } from '../configState'
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { expand } from '../utils'
|
||||||
|
|
||||||
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
|
export default function Nav(): null {
|
||||||
|
let thresholdNums: HTMLSpanElement[] = []
|
||||||
|
let indexNums: HTMLSpanElement[] = []
|
||||||
|
let decButton: HTMLButtonElement | undefined
|
||||||
|
let incButton: HTMLButtonElement | undefined
|
||||||
|
let controller: AbortController | undefined
|
||||||
|
|
||||||
|
const imageState = useImageState()
|
||||||
|
const [config, { incThreshold, decThreshold }] = useConfigState()
|
||||||
|
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 })
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
import { type Power3, type gsap } from 'gsap'
|
|
||||||
|
|
||||||
import { container } from '../container'
|
|
||||||
import { incIndex, isAnimating, navigateVector, state } from '../globalState'
|
|
||||||
import { decrement, increment, loadGsap } from '../globalUtils'
|
|
||||||
import { type ImageJSON } from '../resources'
|
|
||||||
|
|
||||||
import { active, cordHist, isLoading, isOpen } from './state'
|
|
||||||
// eslint-disable-next-line sort-imports
|
|
||||||
import { onMutation, type DesktopImage } from './utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
let imgs: DesktopImage[] = []
|
|
||||||
let last = { x: 0, y: 0 }
|
|
||||||
|
|
||||||
let _gsap: typeof gsap
|
|
||||||
let _Power3: typeof Power3
|
|
||||||
|
|
||||||
let gsapLoaded = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getTrailElsIndex(): number[] {
|
|
||||||
return cordHist.get().map((item) => item.i)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrailCurrentElsIndex(): number[] {
|
|
||||||
return getTrailElsIndex().slice(-state.get().trailLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrailInactiveElsIndex(): number[] {
|
|
||||||
const trailCurrentElsIndex = getTrailCurrentElsIndex()
|
|
||||||
return trailCurrentElsIndex.slice(0, trailCurrentElsIndex.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentElIndex(): number {
|
|
||||||
const trailElsIndex = getTrailElsIndex()
|
|
||||||
return trailElsIndex[trailElsIndex.length - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrevElIndex(): number {
|
|
||||||
const c = cordHist.get()
|
|
||||||
const s = state.get()
|
|
||||||
return decrement(c[c.length - 1].i, s.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextElIndex(): number {
|
|
||||||
const c = cordHist.get()
|
|
||||||
const s = state.get()
|
|
||||||
return increment(c[c.length - 1].i, s.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
// on mouse
|
|
||||||
function onMouse(e: MouseEvent): void {
|
|
||||||
if (isOpen.get() || isAnimating.get()) return
|
|
||||||
if (!gsapLoaded) {
|
|
||||||
loadLib()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const cord = { x: e.clientX, y: e.clientY }
|
|
||||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
|
||||||
|
|
||||||
if (travelDist > state.get().threshold) {
|
|
||||||
last = cord
|
|
||||||
incIndex()
|
|
||||||
|
|
||||||
const newHist = { i: state.get().index, ...cord }
|
|
||||||
cordHist.set([...cordHist.get(), newHist].slice(-state.get().length))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set image position with gsap (in both stage and navigation)
|
|
||||||
function setPositions(): void {
|
|
||||||
const trailElsIndex = getTrailElsIndex()
|
|
||||||
if (trailElsIndex.length === 0 || !gsapLoaded) return
|
|
||||||
|
|
||||||
const elsTrail = getImagesWithIndexArray(trailElsIndex)
|
|
||||||
|
|
||||||
_gsap.set(elsTrail, {
|
|
||||||
x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2,
|
|
||||||
y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2,
|
|
||||||
opacity: (i: number) =>
|
|
||||||
i + 1 + state.get().trailLength <= cordHist.get().length ? 0 : 1,
|
|
||||||
zIndex: (i: number) => i,
|
|
||||||
scale: 0.6
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isOpen.get()) {
|
|
||||||
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
|
|
||||||
elc.classList.add('hide') // hide image to prevent flash
|
|
||||||
const indexArrayToHires: number[] = []
|
|
||||||
switch (navigateVector.get()) {
|
|
||||||
case 'prev':
|
|
||||||
indexArrayToHires.push(getPrevElIndex())
|
|
||||||
break
|
|
||||||
case 'next':
|
|
||||||
indexArrayToHires.push(getNextElIndex())
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
hires(getImagesWithIndexArray(indexArrayToHires)) // preload
|
|
||||||
setLoaderForImage(elc)
|
|
||||||
_gsap.set(imgs, { opacity: 0 })
|
|
||||||
_gsap.set(elc, { opacity: 1, x: 0, y: 0, scale: 1 })
|
|
||||||
} else {
|
|
||||||
lores(elsTrail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// open image into navigation
|
|
||||||
function expandImage(): void {
|
|
||||||
if (isAnimating.get()) return
|
|
||||||
|
|
||||||
isOpen.set(true)
|
|
||||||
isAnimating.set(true)
|
|
||||||
|
|
||||||
const elcIndex = getCurrentElIndex()
|
|
||||||
const elc = getImagesWithIndexArray([elcIndex])[0]
|
|
||||||
// don't hide here because we want a better transition
|
|
||||||
// elc.classList.add('hide')
|
|
||||||
|
|
||||||
hires(getImagesWithIndexArray([elcIndex, getPrevElIndex(), getNextElIndex()]))
|
|
||||||
setLoaderForImage(elc)
|
|
||||||
|
|
||||||
const tl = _gsap.timeline()
|
|
||||||
const trailInactiveEls = getImagesWithIndexArray(getTrailInactiveElsIndex())
|
|
||||||
// move down and hide trail inactive
|
|
||||||
tl.to(trailInactiveEls, {
|
|
||||||
y: '+=20',
|
|
||||||
ease: _Power3.easeIn,
|
|
||||||
stagger: 0.075,
|
|
||||||
duration: 0.3,
|
|
||||||
delay: 0.1,
|
|
||||||
opacity: 0
|
|
||||||
})
|
|
||||||
// current move to center
|
|
||||||
tl.to(elc, {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
ease: _Power3.easeInOut,
|
|
||||||
duration: 0.7,
|
|
||||||
delay: 0.3
|
|
||||||
})
|
|
||||||
// current expand
|
|
||||||
tl.to(elc, {
|
|
||||||
delay: 0.1,
|
|
||||||
scale: 1,
|
|
||||||
ease: _Power3.easeInOut
|
|
||||||
})
|
|
||||||
// finished
|
|
||||||
tl.then(() => {
|
|
||||||
isAnimating.set(false)
|
|
||||||
}).catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// close navigation and back to stage
|
|
||||||
export function minimizeImage(): void {
|
|
||||||
if (isAnimating.get()) return
|
|
||||||
|
|
||||||
isOpen.set(false)
|
|
||||||
isAnimating.set(true)
|
|
||||||
navigateVector.set('none') // cleanup
|
|
||||||
|
|
||||||
lores(
|
|
||||||
getImagesWithIndexArray([...getTrailInactiveElsIndex(), ...[getCurrentElIndex()]])
|
|
||||||
)
|
|
||||||
|
|
||||||
const tl = _gsap.timeline()
|
|
||||||
const elc = getImagesWithIndexArray([getCurrentElIndex()])[0]
|
|
||||||
const elsTrailInactive = getImagesWithIndexArray(getTrailInactiveElsIndex())
|
|
||||||
// shrink current
|
|
||||||
tl.to(elc, {
|
|
||||||
scale: 0.6,
|
|
||||||
duration: 0.6,
|
|
||||||
ease: _Power3.easeInOut
|
|
||||||
})
|
|
||||||
// move current to original position
|
|
||||||
tl.to(elc, {
|
|
||||||
delay: 0.3,
|
|
||||||
duration: 0.7,
|
|
||||||
ease: _Power3.easeInOut,
|
|
||||||
x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2,
|
|
||||||
y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2
|
|
||||||
})
|
|
||||||
// show trail inactive
|
|
||||||
tl.to(elsTrailInactive, {
|
|
||||||
y: '-=20',
|
|
||||||
ease: _Power3.easeOut,
|
|
||||||
stagger: -0.1,
|
|
||||||
duration: 0.3,
|
|
||||||
opacity: 1
|
|
||||||
})
|
|
||||||
// finished
|
|
||||||
tl.then(() => {
|
|
||||||
isAnimating.set(false)
|
|
||||||
}).catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initStage(ijs: ImageJSON[]): void {
|
|
||||||
// create stage element
|
|
||||||
createStage(ijs)
|
|
||||||
// get stage
|
|
||||||
const stage = document.getElementsByClassName('stage').item(0) as HTMLDivElement
|
|
||||||
// get image elements
|
|
||||||
imgs = Array.from(stage.getElementsByTagName('img')) as DesktopImage[]
|
|
||||||
imgs.forEach((img, i) => {
|
|
||||||
// preload first 5 images on page load
|
|
||||||
if (i < 5) {
|
|
||||||
img.src = img.dataset.loUrl
|
|
||||||
}
|
|
||||||
// lores preloader for rest of the images
|
|
||||||
onMutation(img, (mutations, observer) => {
|
|
||||||
mutations.every((mutation) => {
|
|
||||||
// if open or animating, skip
|
|
||||||
if (isOpen.get() || isAnimating.get()) return true
|
|
||||||
// if mutation is not about style attribute, skip
|
|
||||||
if (mutation.attributeName !== 'style') return true
|
|
||||||
const opacity = parseFloat(img.style.opacity)
|
|
||||||
// if opacity is not 1, skip
|
|
||||||
if (opacity !== 1) return true
|
|
||||||
// preload the i + 5th image
|
|
||||||
if (i + 5 < imgs.length) {
|
|
||||||
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
|
||||||
}
|
|
||||||
// disconnect observer and return false to break the loop
|
|
||||||
observer.disconnect()
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// event listeners
|
|
||||||
stage.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
expandImage()
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
stage.addEventListener(
|
|
||||||
'keydown',
|
|
||||||
() => {
|
|
||||||
expandImage()
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
window.addEventListener('mousemove', onMouse, { passive: true })
|
|
||||||
// watchers
|
|
||||||
isOpen.addWatcher((o) => {
|
|
||||||
active.set(o && !isAnimating.get())
|
|
||||||
})
|
|
||||||
isAnimating.addWatcher((o) => {
|
|
||||||
active.set(isOpen.get() && !o)
|
|
||||||
})
|
|
||||||
cordHist.addWatcher((_) => {
|
|
||||||
setPositions()
|
|
||||||
})
|
|
||||||
// dynamic import
|
|
||||||
window.addEventListener(
|
|
||||||
'mousemove',
|
|
||||||
() => {
|
|
||||||
loadLib()
|
|
||||||
},
|
|
||||||
{ once: true, passive: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hepler
|
|
||||||
*/
|
|
||||||
|
|
||||||
function createStage(ijs: ImageJSON[]): void {
|
|
||||||
// create container for images
|
|
||||||
const stage: HTMLDivElement = document.createElement('div')
|
|
||||||
stage.className = 'stage'
|
|
||||||
// append images to container
|
|
||||||
for (const ij of ijs) {
|
|
||||||
const e = document.createElement('img') as DesktopImage
|
|
||||||
e.height = ij.loImgH
|
|
||||||
e.width = ij.loImgW
|
|
||||||
// set data attributes
|
|
||||||
e.dataset.hiUrl = ij.hiUrl
|
|
||||||
e.dataset.hiImgH = ij.hiImgH.toString()
|
|
||||||
e.dataset.hiImgW = ij.hiImgW.toString()
|
|
||||||
e.dataset.loUrl = ij.loUrl
|
|
||||||
e.dataset.loImgH = ij.loImgH.toString()
|
|
||||||
e.dataset.loImgW = ij.loImgW.toString()
|
|
||||||
e.alt = ij.alt
|
|
||||||
// append
|
|
||||||
stage.append(e)
|
|
||||||
}
|
|
||||||
container.append(stage)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImagesWithIndexArray(indexArray: number[]): DesktopImage[] {
|
|
||||||
return indexArray.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 setLoaderForImage(e: HTMLImageElement): void {
|
|
||||||
if (!e.complete) {
|
|
||||||
isLoading.set(true)
|
|
||||||
e.addEventListener(
|
|
||||||
'load',
|
|
||||||
() => {
|
|
||||||
isLoading.set(false)
|
|
||||||
e.classList.remove('hide')
|
|
||||||
},
|
|
||||||
{ once: true, passive: true }
|
|
||||||
)
|
|
||||||
e.addEventListener(
|
|
||||||
'error',
|
|
||||||
() => {
|
|
||||||
isLoading.set(false)
|
|
||||||
},
|
|
||||||
{ once: true, passive: true }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
e.classList.remove('hide')
|
|
||||||
isLoading.set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadLib(): void {
|
|
||||||
loadGsap()
|
|
||||||
.then((g) => {
|
|
||||||
_gsap = g[0]
|
|
||||||
_Power3 = g[1]
|
|
||||||
gsapLoaded = true
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
235
assets/ts/desktop/stage.tsx
Normal file
235
assets/ts/desktop/stage.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { type gsap } from 'gsap'
|
||||||
|
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { useConfigState } from '../configState'
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { increment, loadGsap } from '../utils'
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let _gsap: typeof gsap
|
||||||
|
let gsapPromise: Promise<void> | undefined
|
||||||
|
|
||||||
|
const imageState = useImageState()
|
||||||
|
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 abortController: AbortController | undefined
|
||||||
|
let gsapLoaded = 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) => {
|
||||||
|
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 travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||||
|
|
||||||
|
if (travelDist > config().threshold) {
|
||||||
|
const nextIndex = increment(desktop.index(), length)
|
||||||
|
|
||||||
|
last = cord
|
||||||
|
setIndex(nextIndex)
|
||||||
|
setCordHist((prev) => [...prev, { i: nextIndex, ...cord }].slice(-length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick: () => Promise<void> = async () => {
|
||||||
|
if (!gsapLoaded) {
|
||||||
|
await ensureGsapReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktop.isAnimating() || !gsapLoaded) return
|
||||||
|
if (desktop.index() < 0 || desktop.cordHist().length === 0) return
|
||||||
|
setIsOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPosition: () => void = () => {
|
||||||
|
syncStagePosition({
|
||||||
|
gsap: _gsap,
|
||||||
|
imgs,
|
||||||
|
cordHist: desktop.cordHist(),
|
||||||
|
trailLength: config().trailLength,
|
||||||
|
length: imageState().length,
|
||||||
|
isOpen: desktop.isOpen(),
|
||||||
|
navVector: desktop.navVector(),
|
||||||
|
mounted,
|
||||||
|
setIsLoading
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandImage: () => Promise<void> = async () => {
|
||||||
|
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||||
|
|
||||||
|
await expandStage({
|
||||||
|
gsap: _gsap,
|
||||||
|
imgs,
|
||||||
|
cordHist: desktop.cordHist(),
|
||||||
|
trailLength: config().trailLength,
|
||||||
|
length: imageState().length,
|
||||||
|
mounted,
|
||||||
|
setIsLoading,
|
||||||
|
setIsAnimating
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimizeImage: () => Promise<void> = async () => {
|
||||||
|
if (!mounted || !gsapLoaded) throw new Error('not mounted or gsap not loaded')
|
||||||
|
|
||||||
|
setNavVector('none')
|
||||||
|
|
||||||
|
await minimizeStage({
|
||||||
|
gsap: _gsap,
|
||||||
|
imgs,
|
||||||
|
cordHist: desktop.cordHist(),
|
||||||
|
trailLength: config().trailLength,
|
||||||
|
mounted,
|
||||||
|
setIsAnimating
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
imgs.forEach((img, i) => {
|
||||||
|
if (i < 5) {
|
||||||
|
img.src = img.dataset.loUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
onMutation(img, (mutation) => {
|
||||||
|
if (desktop.isOpen() || desktop.isAnimating()) return false
|
||||||
|
if (mutation.attributeName !== 'style') return false
|
||||||
|
|
||||||
|
const opacity = parseFloat(img.style.opacity)
|
||||||
|
if (opacity !== 1) return false
|
||||||
|
|
||||||
|
if (i + 5 < imgs.length) {
|
||||||
|
imgs[i + 5].src = imgs[i + 5].dataset.loUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', () => void ensureGsapReady(), {
|
||||||
|
passive: true,
|
||||||
|
once: true
|
||||||
|
})
|
||||||
|
window.addEventListener('pointerdown', () => void ensureGsapReady(), {
|
||||||
|
passive: true,
|
||||||
|
once: true
|
||||||
|
})
|
||||||
|
window.addEventListener('click', () => void ensureGsapReady(), {
|
||||||
|
passive: true,
|
||||||
|
once: true
|
||||||
|
})
|
||||||
|
|
||||||
|
abortController = new AbortController()
|
||||||
|
const abortSignal = abortController.signal
|
||||||
|
window.addEventListener('mousemove', onMouse, {
|
||||||
|
passive: true,
|
||||||
|
signal: abortSignal
|
||||||
|
})
|
||||||
|
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => desktop.cordHist(),
|
||||||
|
() => {
|
||||||
|
setPosition()
|
||||||
|
},
|
||||||
|
{ defer: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
desktop.isOpen,
|
||||||
|
async (isOpen) => {
|
||||||
|
if (desktop.isAnimating()) return
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
if (desktop.index() < 0 || desktop.cordHist().length === 0) {
|
||||||
|
setIsOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await expandImage()
|
||||||
|
.catch(() => {
|
||||||
|
setIsOpen(false)
|
||||||
|
setIsAnimating(false)
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
abortController?.abort()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await minimizeImage()
|
||||||
|
.catch(() => {
|
||||||
|
void 0
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
abortController = new AbortController()
|
||||||
|
const abortSignal = abortController.signal
|
||||||
|
window.addEventListener('mousemove', onMouse, {
|
||||||
|
passive: true,
|
||||||
|
signal: abortSignal
|
||||||
|
})
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ defer: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="stage" onClick={onClick} onKeyDown={onClick}>
|
||||||
|
<For each={imageState().images}>
|
||||||
|
{(ij, i) => (
|
||||||
|
<img
|
||||||
|
ref={imgs[i()]}
|
||||||
|
height={ij.loImgH}
|
||||||
|
width={ij.loImgW}
|
||||||
|
data-hi-url={ij.hiUrl}
|
||||||
|
data-hi-img-h={ij.hiImgH}
|
||||||
|
data-hi-img-w={ij.hiImgW}
|
||||||
|
data-lo-url={ij.loUrl}
|
||||||
|
data-lo-img-h={ij.loImgH}
|
||||||
|
data-lo-img-w={ij.loImgW}
|
||||||
|
alt={ij.alt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
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,194 +0,0 @@
|
|||||||
import { container } from '../container'
|
|
||||||
import { decIndex, incIndex, isAnimating, navigateVector, state } from '../globalState'
|
|
||||||
import { decrement, increment } from '../globalUtils'
|
|
||||||
|
|
||||||
import { setCustomCursor } from './customCursor'
|
|
||||||
import { minimizeImage } from './stage'
|
|
||||||
import { active, cordHist, isLoading, isOpen } from './state'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* types
|
|
||||||
*/
|
|
||||||
|
|
||||||
type NavItem = (typeof navItems)[number]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
container.dataset.prev,
|
|
||||||
container.dataset.close,
|
|
||||||
container.dataset.next
|
|
||||||
] as const
|
|
||||||
const loadingText = container.dataset.loading + '...'
|
|
||||||
let loadedText = ''
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function handleClick(type: NavItem): void {
|
|
||||||
if (type === navItems[0]) {
|
|
||||||
prevImage()
|
|
||||||
} else if (type === navItems[1]) {
|
|
||||||
minimizeImage()
|
|
||||||
} else {
|
|
||||||
nextImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKey(e: KeyboardEvent): void {
|
|
||||||
if (isOpen.get() || isAnimating.get()) return
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowLeft':
|
|
||||||
prevImage()
|
|
||||||
break
|
|
||||||
case 'Escape':
|
|
||||||
minimizeImage()
|
|
||||||
break
|
|
||||||
case 'ArrowRight':
|
|
||||||
nextImage()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initStageNav(): void {
|
|
||||||
// isLoading
|
|
||||||
isLoading.addWatcher((o) => {
|
|
||||||
if (o) setCustomCursor(loadingText)
|
|
||||||
else setCustomCursor(loadedText)
|
|
||||||
})
|
|
||||||
// navOverlay
|
|
||||||
const navOverlay = document.createElement('div')
|
|
||||||
navOverlay.className = 'navOverlay'
|
|
||||||
for (const [index, navItem] of navItems.entries()) {
|
|
||||||
const overlay = document.createElement('div')
|
|
||||||
overlay.className = 'overlay'
|
|
||||||
const isClose = index === 1
|
|
||||||
// close
|
|
||||||
if (isClose) {
|
|
||||||
overlay.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
handleCloseClick(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
overlay.addEventListener(
|
|
||||||
'keydown',
|
|
||||||
() => {
|
|
||||||
handleCloseClick(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
overlay.addEventListener(
|
|
||||||
'mouseover',
|
|
||||||
() => {
|
|
||||||
handleCloseHover(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
overlay.addEventListener(
|
|
||||||
'focus',
|
|
||||||
() => {
|
|
||||||
handleCloseHover(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// prev and next
|
|
||||||
else {
|
|
||||||
overlay.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
handlePNClick(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
overlay.addEventListener(
|
|
||||||
'keydown',
|
|
||||||
() => {
|
|
||||||
handlePNClick(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
overlay.addEventListener(
|
|
||||||
'mouseover',
|
|
||||||
() => {
|
|
||||||
handlePNHover(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
overlay.addEventListener(
|
|
||||||
'focus',
|
|
||||||
() => {
|
|
||||||
handlePNHover(navItem)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
navOverlay.append(overlay)
|
|
||||||
}
|
|
||||||
active.addWatcher(() => {
|
|
||||||
if (active.get()) {
|
|
||||||
navOverlay.classList.add('active')
|
|
||||||
} else {
|
|
||||||
navOverlay.classList.remove('active')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
container.append(navOverlay)
|
|
||||||
window.addEventListener('keydown', handleKey, { passive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hepler
|
|
||||||
*/
|
|
||||||
|
|
||||||
function nextImage(): void {
|
|
||||||
if (isAnimating.get()) return
|
|
||||||
navigateVector.set('next')
|
|
||||||
cordHist.set(
|
|
||||||
cordHist.get().map((item) => {
|
|
||||||
return { ...item, i: increment(item.i, state.get().length) }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
incIndex()
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevImage(): void {
|
|
||||||
if (isAnimating.get()) return
|
|
||||||
navigateVector.set('prev')
|
|
||||||
cordHist.set(
|
|
||||||
cordHist.get().map((item) => {
|
|
||||||
return { ...item, i: decrement(item.i, state.get().length) }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
decIndex()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCloseClick(navItem: NavItem): void {
|
|
||||||
handleClick(navItem)
|
|
||||||
isLoading.set(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCloseHover(navItem: NavItem): void {
|
|
||||||
loadedText = navItem
|
|
||||||
setCustomCursor(navItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePNClick(navItem: NavItem): void {
|
|
||||||
if (!isLoading.get()) handleClick(navItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePNHover(navItem: NavItem): void {
|
|
||||||
loadedText = navItem
|
|
||||||
if (isLoading.get()) setCustomCursor(loadingText)
|
|
||||||
else setCustomCursor(navItem)
|
|
||||||
}
|
|
||||||
107
assets/ts/desktop/stageNav.tsx
Normal file
107
assets/ts/desktop/stageNav.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { For, createEffect, createMemo, on, onCleanup, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { decrement, increment } from '../utils'
|
||||||
|
|
||||||
|
import { useDesktopState } from './state'
|
||||||
|
|
||||||
|
export default function StageNav(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
prevText: string
|
||||||
|
closeText: string
|
||||||
|
nextText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
// types
|
||||||
|
type NavItem = (typeof navItems)[number]
|
||||||
|
|
||||||
|
// variables
|
||||||
|
let controller: AbortController | undefined
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
const navItems = [props.prevText, props.closeText, props.nextText] as const
|
||||||
|
|
||||||
|
// states
|
||||||
|
const imageState = useImageState()
|
||||||
|
const [
|
||||||
|
desktop,
|
||||||
|
{ incIndex, decIndex, setCordHist, setHoverText, setIsOpen, setNavVector }
|
||||||
|
] = useDesktopState()
|
||||||
|
|
||||||
|
const active = createMemo(() => desktop.isOpen() && !desktop.isAnimating())
|
||||||
|
|
||||||
|
const prevImage: () => void = () => {
|
||||||
|
setNavVector('prev')
|
||||||
|
setCordHist((c) =>
|
||||||
|
c.map((item) => {
|
||||||
|
return { ...item, i: decrement(item.i, imageState().length) }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
decIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImage: () => void = () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextImage: () => void = () => {
|
||||||
|
setNavVector('next')
|
||||||
|
setCordHist((c) =>
|
||||||
|
c.map((item) => {
|
||||||
|
return { ...item, i: increment(item.i, imageState().length) }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
incIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick: (item: NavItem) => void = (item) => {
|
||||||
|
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||||
|
if (item === navItems[0]) prevImage()
|
||||||
|
else if (item === navItems[1]) closeImage()
|
||||||
|
else nextImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKey: (e: KeyboardEvent) => void = (e) => {
|
||||||
|
if (!desktop.isOpen() || desktop.isAnimating()) return
|
||||||
|
if (e.key === 'ArrowLeft') prevImage()
|
||||||
|
else if (e.key === 'Escape') closeImage()
|
||||||
|
else if (e.key === 'ArrowRight') nextImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(desktop.isOpen, (isOpen) => {
|
||||||
|
controller?.abort()
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
controller = new AbortController()
|
||||||
|
const abortSignal = controller.signal
|
||||||
|
window.addEventListener('keydown', handleKey, {
|
||||||
|
passive: true,
|
||||||
|
signal: abortSignal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
controller?.abort()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="navOverlay" classList={{ active: active() }}>
|
||||||
|
<For each={navItems}>
|
||||||
|
{(item) => (
|
||||||
|
<div
|
||||||
|
class="overlay"
|
||||||
|
onClick={() => {
|
||||||
|
handleClick(item)
|
||||||
|
}}
|
||||||
|
onFocus={() => setHoverText(item)}
|
||||||
|
onMouseOver={() => setHoverText(item)}
|
||||||
|
tabIndex="-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
import { Watchable } from '../globalUtils'
|
import {
|
||||||
|
createComponent,
|
||||||
|
createContext,
|
||||||
|
createSignal,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
type JSX,
|
||||||
|
type Setter
|
||||||
|
} from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
/**
|
import { useImageState } from '../imageState'
|
||||||
* types
|
import { decrement, increment, type Vector } from '../utils'
|
||||||
*/
|
|
||||||
|
|
||||||
export interface HistoryItem {
|
export interface HistoryItem {
|
||||||
i: number
|
i: number
|
||||||
@@ -10,11 +18,79 @@ export interface HistoryItem {
|
|||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface DesktopState {
|
||||||
* variables
|
index: Accessor<number>
|
||||||
*/
|
cordHist: Accessor<HistoryItem[]>
|
||||||
|
hoverText: Accessor<string>
|
||||||
|
isOpen: Accessor<boolean>
|
||||||
|
isAnimating: Accessor<boolean>
|
||||||
|
isLoading: Accessor<boolean>
|
||||||
|
navVector: Accessor<Vector>
|
||||||
|
}
|
||||||
|
|
||||||
export const cordHist = new Watchable<HistoryItem[]>([])
|
export type DesktopStateContextType = readonly [
|
||||||
export const isOpen = new Watchable<boolean>(false)
|
DesktopState,
|
||||||
export const active = new Watchable<boolean>(false)
|
{
|
||||||
export const isLoading = new Watchable<boolean>(false)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* interfaces
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface DesktopImage extends HTMLImageElement {
|
|
||||||
dataset: {
|
|
||||||
hiUrl: string
|
|
||||||
hiImgH: string
|
|
||||||
hiImgW: string
|
|
||||||
loUrl: string
|
|
||||||
loImgH: string
|
|
||||||
loImgW: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function onMutation<T extends HTMLElement>(
|
|
||||||
element: T,
|
|
||||||
callback: (arg0: MutationRecord[], arg1: MutationObserver) => void,
|
|
||||||
observeOptions: MutationObserverInit = { attributes: true }
|
|
||||||
): void {
|
|
||||||
new MutationObserver((mutations, observer) => {
|
|
||||||
callback(mutations, observer)
|
|
||||||
}).observe(element, observeOptions)
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import {
|
|
||||||
Watchable,
|
|
||||||
decrement,
|
|
||||||
getThresholdSessionIndex,
|
|
||||||
increment
|
|
||||||
} from './globalUtils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* types
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type State = typeof defaultState
|
|
||||||
export type NavVec = 'next' | 'none' | 'prev'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
const thresholds = [
|
|
||||||
{ threshold: 20, trailLength: 20 },
|
|
||||||
{ threshold: 40, trailLength: 10 },
|
|
||||||
{ threshold: 80, trailLength: 5 },
|
|
||||||
{ threshold: 140, trailLength: 5 },
|
|
||||||
{ threshold: 200, trailLength: 5 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
index: -1,
|
|
||||||
length: 0,
|
|
||||||
threshold: thresholds[getThresholdSessionIndex()].threshold,
|
|
||||||
trailLength: thresholds[getThresholdSessionIndex()].trailLength
|
|
||||||
}
|
|
||||||
|
|
||||||
export const state = new Watchable<State>(defaultState)
|
|
||||||
export const isAnimating = new Watchable<boolean>(false)
|
|
||||||
export const navigateVector = new Watchable<NavVec>('none')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initState(length: number): void {
|
|
||||||
const s = state.get()
|
|
||||||
s.length = length
|
|
||||||
updateThreshold(s, 0)
|
|
||||||
state.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setIndex(index: number): void {
|
|
||||||
const s = state.get()
|
|
||||||
s.index = index
|
|
||||||
state.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function incIndex(): void {
|
|
||||||
const s = state.get()
|
|
||||||
s.index = increment(s.index, s.length)
|
|
||||||
state.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decIndex(): void {
|
|
||||||
const s = state.get()
|
|
||||||
s.index = decrement(s.index, s.length)
|
|
||||||
state.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function incThreshold(): void {
|
|
||||||
let s = state.get()
|
|
||||||
s = updateThreshold(s, 1)
|
|
||||||
state.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decThreshold(): void {
|
|
||||||
let s = state.get()
|
|
||||||
s = updateThreshold(s, -1)
|
|
||||||
state.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper
|
|
||||||
*/
|
|
||||||
|
|
||||||
function updateThreshold(state: State, inc: number): State {
|
|
||||||
const i = thresholds.findIndex((t) => state.threshold === t.threshold) + inc
|
|
||||||
// out of bounds
|
|
||||||
if (i < 0 || i >= thresholds.length) return state
|
|
||||||
// storage the index so we can restore it even if we go to another page
|
|
||||||
sessionStorage.setItem('thresholdsIndex', i.toString())
|
|
||||||
const newItems = thresholds[i]
|
|
||||||
return { ...state, ...newItems }
|
|
||||||
}
|
|
||||||
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,54 +0,0 @@
|
|||||||
import { initContainer } from './container'
|
|
||||||
import { initState } from './globalState'
|
|
||||||
import { initNav } from './nav'
|
|
||||||
import { initResources } from './resources'
|
|
||||||
|
|
||||||
// this is the main entry point for the app
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
main().catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
|
||||||
initContainer()
|
|
||||||
const ijs = await initResources()
|
|
||||||
initState(ijs.length)
|
|
||||||
initNav()
|
|
||||||
|
|
||||||
if (ijs.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: it seems firefox and chromnium don't like top layer await
|
|
||||||
// so we are using import then instead
|
|
||||||
if (!isMobile()) {
|
|
||||||
await import('./desktop/init')
|
|
||||||
.then((d) => {
|
|
||||||
d.initDesktop(ijs)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await import('./mobile/init')
|
|
||||||
.then((m) => {
|
|
||||||
m.initMobile(ijs)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hepler
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isMobile(): boolean {
|
|
||||||
return window.matchMedia('(hover: none)').matches
|
|
||||||
}
|
|
||||||
91
assets/ts/main.tsx
Normal file
91
assets/ts/main.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Match, Show, Switch, createResource, lazy, type JSX } from 'solid-js'
|
||||||
|
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 '../scss/style.scss'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Container extends HTMLDivElement {
|
||||||
|
dataset: {
|
||||||
|
next: string
|
||||||
|
close: string
|
||||||
|
prev: string
|
||||||
|
loading: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// container
|
||||||
|
const container = document.getElementsByClassName('container')[0] as Container
|
||||||
|
|
||||||
|
// lazy components
|
||||||
|
const Desktop = lazy(async () => await import('./desktop/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 {
|
||||||
|
// variables
|
||||||
|
const [ijs] = createResource(getImageJSON)
|
||||||
|
const ua = window.navigator.userAgent.toLowerCase()
|
||||||
|
const hasTouchInput = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0
|
||||||
|
const hasTouchLayout =
|
||||||
|
window.matchMedia('(pointer: coarse)').matches ||
|
||||||
|
window.matchMedia('(hover: none)').matches
|
||||||
|
const isMobileUA = /android|iphone|ipad|ipod|mobile/.test(ua)
|
||||||
|
const isWindowsDesktop = /windows nt/.test(ua)
|
||||||
|
const isMobile = isMobileUA || (hasTouchInput && hasTouchLayout && !isWindowsDesktop)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={ijs.state === 'ready'}>
|
||||||
|
<ImageStateProvider images={ijs() ?? []}>
|
||||||
|
<ConfigStateProvider>
|
||||||
|
<AppContent
|
||||||
|
isMobile={isMobile}
|
||||||
|
prevText={container.dataset.prev}
|
||||||
|
closeText={container.dataset.close}
|
||||||
|
nextText={container.dataset.next}
|
||||||
|
loadingText={container.dataset.loading}
|
||||||
|
/>
|
||||||
|
</ConfigStateProvider>
|
||||||
|
</ImageStateProvider>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(() => <Main />, container)
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { container } from '../container'
|
|
||||||
import { setIndex } from '../globalState'
|
|
||||||
import { type ImageJSON } from '../resources'
|
|
||||||
|
|
||||||
import { slideUp } from './gallery'
|
|
||||||
import { mounted } from './state'
|
|
||||||
// eslint-disable-next-line sort-imports
|
|
||||||
import { getRandom, onIntersection, type MobileImage } from './utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
export let imgs: MobileImage[] = []
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function handleClick(i: number): void {
|
|
||||||
setIndex(i)
|
|
||||||
slideUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initCollection(ijs: ImageJSON[]): void {
|
|
||||||
createCollection(ijs)
|
|
||||||
// get container
|
|
||||||
const collection = document
|
|
||||||
.getElementsByClassName('collection')
|
|
||||||
.item(0) as HTMLDivElement
|
|
||||||
// add watcher
|
|
||||||
mounted.addWatcher((o) => {
|
|
||||||
if (o) {
|
|
||||||
collection.classList.remove('hidden')
|
|
||||||
} else {
|
|
||||||
collection.classList.add('hidden')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// get image elements
|
|
||||||
imgs = Array.from(collection.getElementsByTagName('img')) as MobileImage[]
|
|
||||||
// add event listeners
|
|
||||||
imgs.forEach((img, i) => {
|
|
||||||
// preload first 5 images on page load
|
|
||||||
if (i < 5) {
|
|
||||||
img.src = img.dataset.src
|
|
||||||
}
|
|
||||||
// event listeners
|
|
||||||
img.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
handleClick(i)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
img.addEventListener(
|
|
||||||
'keydown',
|
|
||||||
() => {
|
|
||||||
handleClick(i)
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
// preload
|
|
||||||
onIntersection(img, (entries, observer) => {
|
|
||||||
entries.every((entry) => {
|
|
||||||
// no intersection, skip
|
|
||||||
if (entry.intersectionRatio <= 0) return true
|
|
||||||
// preload the i + 5th image
|
|
||||||
if (i + 5 < imgs.length) {
|
|
||||||
imgs[i + 5].src = imgs[i + 5].dataset.src
|
|
||||||
}
|
|
||||||
// disconnect observer and return false to break the loop
|
|
||||||
observer.disconnect()
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper
|
|
||||||
*/
|
|
||||||
|
|
||||||
function createCollection(ijs: ImageJSON[]): void {
|
|
||||||
// create container for images
|
|
||||||
const _collection: HTMLDivElement = document.createElement('div')
|
|
||||||
_collection.className = 'collection'
|
|
||||||
// append images to container
|
|
||||||
for (const [i, ij] of ijs.entries()) {
|
|
||||||
// random x and y
|
|
||||||
const x = i !== 0 ? getRandom(-25, 25) : 0
|
|
||||||
const y = i !== 0 ? getRandom(-30, 30) : 0
|
|
||||||
// element
|
|
||||||
const e = document.createElement('img') as MobileImage
|
|
||||||
e.dataset.src = ij.loUrl
|
|
||||||
e.height = ij.loImgH
|
|
||||||
e.width = ij.loImgW
|
|
||||||
e.alt = ij.alt
|
|
||||||
e.style.transform = `translate3d(${x}%, ${y - 50}%, 0)`
|
|
||||||
_collection.append(e)
|
|
||||||
}
|
|
||||||
container.append(_collection)
|
|
||||||
}
|
|
||||||
120
assets/ts/mobile/collection.tsx
Normal file
120
assets/ts/mobile/collection.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { For, createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
|
||||||
|
import type { MobileImage } from './layout'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
|
function getRandom(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIntersection<T extends HTMLElement>(
|
||||||
|
element: T,
|
||||||
|
trigger: (arg0: IntersectionObserverEntry) => boolean
|
||||||
|
): void {
|
||||||
|
new IntersectionObserver((entries, observer) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (trigger(entry)) {
|
||||||
|
observer.disconnect()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Collection(): JSX.Element {
|
||||||
|
// variables
|
||||||
|
const imageState = useImageState()
|
||||||
|
const imgs: MobileImage[] = Array<MobileImage>(imageState().length)
|
||||||
|
|
||||||
|
// states
|
||||||
|
const [mobile, { setIndex, setIsOpen }] = useMobileState()
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
const handleClick: (i: number) => void = (i) => {
|
||||||
|
if (mobile.isAnimating()) return
|
||||||
|
setIndex(i)
|
||||||
|
setIsOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToActive: () => void = () => {
|
||||||
|
const index = mobile.index()
|
||||||
|
|
||||||
|
if (index < 0) return
|
||||||
|
imgs[index].scrollIntoView({ behavior: 'auto', block: 'center' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// effects
|
||||||
|
onMount(() => {
|
||||||
|
imgs.forEach((img, i) => {
|
||||||
|
// preload first 5 images on page load
|
||||||
|
if (i < 5) {
|
||||||
|
img.src = img.dataset.src
|
||||||
|
}
|
||||||
|
// event listeners
|
||||||
|
img.addEventListener(
|
||||||
|
'click',
|
||||||
|
() => {
|
||||||
|
handleClick(i)
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
)
|
||||||
|
img.addEventListener(
|
||||||
|
'keydown',
|
||||||
|
() => {
|
||||||
|
handleClick(i)
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
)
|
||||||
|
// preload
|
||||||
|
onIntersection(img, (entry) => {
|
||||||
|
// no intersection, hold
|
||||||
|
if (entry.intersectionRatio <= 0) return false
|
||||||
|
// preload the i + 5th image, if it exists
|
||||||
|
if (i + 5 < imgs.length) {
|
||||||
|
imgs[i + 5].src = imgs[i + 5].dataset.src
|
||||||
|
}
|
||||||
|
// triggered
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
mobile.isOpen,
|
||||||
|
() => {
|
||||||
|
if (!mobile.isOpen()) scrollToActive() // scroll to active when closed
|
||||||
|
},
|
||||||
|
{ defer: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="collection">
|
||||||
|
<For each={imageState().images}>
|
||||||
|
{(ij, i) => (
|
||||||
|
<img
|
||||||
|
ref={imgs[i()]}
|
||||||
|
height={ij.loImgH}
|
||||||
|
width={ij.loImgW}
|
||||||
|
data-src={ij.loUrl}
|
||||||
|
alt={ij.alt}
|
||||||
|
style={{
|
||||||
|
transform: `translate3d(${i() !== 0 ? getRandom(-25, 25) : 0}%, ${i() !== 0 ? getRandom(-35, 35) : 0}%, 0)`
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
handleClick(i())
|
||||||
|
}}
|
||||||
|
onKeyDown={() => {
|
||||||
|
handleClick(i())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
import { type Power3, type gsap } from 'gsap'
|
|
||||||
import { type Swiper } from 'swiper'
|
|
||||||
|
|
||||||
import { container, scrollable } from '../container'
|
|
||||||
import { isAnimating, navigateVector, setIndex, state } from '../globalState'
|
|
||||||
import { expand, loadGsap, removeDuplicates } from '../globalUtils'
|
|
||||||
import { type ImageJSON } from '../resources'
|
|
||||||
|
|
||||||
import { mounted } from './state'
|
|
||||||
// eslint-disable-next-line sort-imports
|
|
||||||
import { capitalizeFirstLetter, loadSwiper, type MobileImage } from './utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
let swiperNode: HTMLDivElement
|
|
||||||
let gallery: HTMLDivElement
|
|
||||||
let curtain: HTMLDivElement
|
|
||||||
let swiper: Swiper
|
|
||||||
let lastIndex = -1
|
|
||||||
let indexDispNums: HTMLSpanElement[] = []
|
|
||||||
let galleryImages: MobileImage[] = []
|
|
||||||
let collectionImages: MobileImage[] = []
|
|
||||||
|
|
||||||
let _Swiper: typeof Swiper
|
|
||||||
let _gsap: typeof gsap
|
|
||||||
let _Power3: typeof Power3
|
|
||||||
|
|
||||||
let libLoaded = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function slideUp(): void {
|
|
||||||
if (isAnimating.get() || !libLoaded) return
|
|
||||||
isAnimating.set(true)
|
|
||||||
|
|
||||||
// load active image
|
|
||||||
galleryLoadImages()
|
|
||||||
|
|
||||||
_gsap.to(curtain, {
|
|
||||||
opacity: 1,
|
|
||||||
duration: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
_gsap.to(gallery, {
|
|
||||||
y: 0,
|
|
||||||
ease: _Power3.easeInOut,
|
|
||||||
duration: 1,
|
|
||||||
delay: 0.4
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollable.set(false)
|
|
||||||
isAnimating.set(false)
|
|
||||||
}, 1400)
|
|
||||||
}
|
|
||||||
|
|
||||||
function slideDown(): void {
|
|
||||||
if (isAnimating.get()) return
|
|
||||||
isAnimating.set(true)
|
|
||||||
scrollToActive()
|
|
||||||
|
|
||||||
_gsap.to(gallery, {
|
|
||||||
y: '100%',
|
|
||||||
ease: _Power3.easeInOut,
|
|
||||||
duration: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
_gsap.to(curtain, {
|
|
||||||
opacity: 0,
|
|
||||||
duration: 1.2,
|
|
||||||
delay: 0.4
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollable.set(true)
|
|
||||||
isAnimating.set(false)
|
|
||||||
}, 1600)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initGallery(ijs: ImageJSON[]): void {
|
|
||||||
// create gallery
|
|
||||||
createGallery(ijs)
|
|
||||||
// get elements
|
|
||||||
indexDispNums = Array.from(
|
|
||||||
document.getElementsByClassName('nav').item(0)?.getElementsByClassName('num') ?? []
|
|
||||||
) as HTMLSpanElement[]
|
|
||||||
swiperNode = document.getElementsByClassName('galleryInner').item(0) as HTMLDivElement
|
|
||||||
gallery = document.getElementsByClassName('gallery').item(0) as HTMLDivElement
|
|
||||||
curtain = document.getElementsByClassName('curtain').item(0) as HTMLDivElement
|
|
||||||
galleryImages = Array.from(gallery.getElementsByTagName('img')) as MobileImage[]
|
|
||||||
collectionImages = Array.from(
|
|
||||||
document
|
|
||||||
.getElementsByClassName('collection')
|
|
||||||
.item(0)
|
|
||||||
?.getElementsByTagName('img') ?? []
|
|
||||||
) as MobileImage[]
|
|
||||||
// state watcher
|
|
||||||
state.addWatcher(() => {
|
|
||||||
const s = state.get()
|
|
||||||
// change slide only when index is changed
|
|
||||||
if (s.index === lastIndex) return
|
|
||||||
else if (lastIndex === -1)
|
|
||||||
navigateVector.set('none') // lastIndex before first set
|
|
||||||
else if (s.index < lastIndex) navigateVector.set('prev')
|
|
||||||
else navigateVector.set('next')
|
|
||||||
changeSlide(s.index)
|
|
||||||
updateIndexText()
|
|
||||||
lastIndex = s.index
|
|
||||||
})
|
|
||||||
// mounted watcher
|
|
||||||
mounted.addWatcher((o) => {
|
|
||||||
if (!o) return
|
|
||||||
scrollable.set(true)
|
|
||||||
})
|
|
||||||
// dynamic import
|
|
||||||
window.addEventListener(
|
|
||||||
'touchstart',
|
|
||||||
() => {
|
|
||||||
loadGsap()
|
|
||||||
.then((g) => {
|
|
||||||
_gsap = g[0]
|
|
||||||
_Power3 = g[1]
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
loadSwiper()
|
|
||||||
.then((s) => {
|
|
||||||
_Swiper = s
|
|
||||||
swiper = new _Swiper(swiperNode, { spaceBetween: 20 })
|
|
||||||
swiper.on('slideChange', ({ realIndex }) => {
|
|
||||||
setIndex(realIndex)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
libLoaded = true
|
|
||||||
},
|
|
||||||
{ once: true, passive: true }
|
|
||||||
)
|
|
||||||
// mounted
|
|
||||||
mounted.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper
|
|
||||||
*/
|
|
||||||
|
|
||||||
function changeSlide(slide: number): void {
|
|
||||||
galleryLoadImages()
|
|
||||||
swiper.slideTo(slide, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToActive(): void {
|
|
||||||
collectionImages[state.get().index].scrollIntoView({
|
|
||||||
block: 'center',
|
|
||||||
behavior: 'auto'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIndexText(): void {
|
|
||||||
const indexValue: string = expand(state.get().index + 1)
|
|
||||||
const indexLength: string = expand(state.get().length)
|
|
||||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
|
||||||
if (i < 4) {
|
|
||||||
e.innerText = indexValue[i]
|
|
||||||
} else {
|
|
||||||
e.innerText = indexLength[i - 4]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function galleryLoadImages(): void {
|
|
||||||
let activeImagesIndex: number[] = []
|
|
||||||
const currentIndex = state.get().index
|
|
||||||
const nextIndex = Math.min(currentIndex + 1, state.get().length - 1)
|
|
||||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
|
||||||
switch (navigateVector.get()) {
|
|
||||||
case 'next':
|
|
||||||
activeImagesIndex = [nextIndex]
|
|
||||||
break
|
|
||||||
case 'prev':
|
|
||||||
activeImagesIndex = [prevIndex]
|
|
||||||
break
|
|
||||||
case 'none':
|
|
||||||
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
removeDuplicates(activeImagesIndex).forEach((i) => {
|
|
||||||
const e = galleryImages[i]
|
|
||||||
if (e.src === e.dataset.src) return // already loaded
|
|
||||||
e.src = e.dataset.src
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function createGallery(ijs: ImageJSON[]): void {
|
|
||||||
/**
|
|
||||||
* gallery
|
|
||||||
* |- galleryInner
|
|
||||||
* |- swiper-wrapper
|
|
||||||
* |- swiper-slide
|
|
||||||
* |- img
|
|
||||||
* |- swiper-slide
|
|
||||||
* |- img
|
|
||||||
* |- ...
|
|
||||||
* |- nav
|
|
||||||
* |- index
|
|
||||||
* |- close
|
|
||||||
*/
|
|
||||||
// swiper wrapper
|
|
||||||
const _swiperWrapper = document.createElement('div')
|
|
||||||
_swiperWrapper.className = 'swiper-wrapper'
|
|
||||||
// loading text
|
|
||||||
const loadingText = container.dataset.loading
|
|
||||||
for (const ij of ijs) {
|
|
||||||
// swiper slide
|
|
||||||
const _swiperSlide = document.createElement('div')
|
|
||||||
_swiperSlide.className = 'swiper-slide'
|
|
||||||
// loading indicator
|
|
||||||
const l = document.createElement('div')
|
|
||||||
l.className = 'loadingText'
|
|
||||||
l.innerText = loadingText
|
|
||||||
// img
|
|
||||||
const e = document.createElement('img') as MobileImage
|
|
||||||
e.dataset.src = ij.hiUrl
|
|
||||||
e.height = ij.hiImgH
|
|
||||||
e.width = ij.hiImgW
|
|
||||||
e.alt = ij.alt
|
|
||||||
e.classList.add('hide')
|
|
||||||
// load event
|
|
||||||
e.addEventListener(
|
|
||||||
'load',
|
|
||||||
() => {
|
|
||||||
e.classList.remove('hide')
|
|
||||||
l.classList.add('hide')
|
|
||||||
},
|
|
||||||
{ once: true, passive: true }
|
|
||||||
)
|
|
||||||
// parent container
|
|
||||||
const p = document.createElement('div')
|
|
||||||
p.className = 'slideContainer'
|
|
||||||
// append
|
|
||||||
p.append(e)
|
|
||||||
p.append(l)
|
|
||||||
_swiperSlide.append(p)
|
|
||||||
_swiperWrapper.append(_swiperSlide)
|
|
||||||
}
|
|
||||||
// swiper node
|
|
||||||
const _swiperNode = document.createElement('div')
|
|
||||||
_swiperNode.className = 'galleryInner'
|
|
||||||
_swiperNode.append(_swiperWrapper)
|
|
||||||
// index
|
|
||||||
const _index = document.createElement('div')
|
|
||||||
_index.insertAdjacentHTML(
|
|
||||||
'afterbegin',
|
|
||||||
`<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>
|
|
||||||
<span>/</span>
|
|
||||||
<span class="num"></span><span class="num"></span><span class="num"></span><span class="num"></span>`
|
|
||||||
)
|
|
||||||
// close
|
|
||||||
const _close = document.createElement('div')
|
|
||||||
_close.innerText = capitalizeFirstLetter(container.dataset.close)
|
|
||||||
_close.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
slideDown()
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
_close.addEventListener(
|
|
||||||
'keydown',
|
|
||||||
() => {
|
|
||||||
slideDown()
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
// nav
|
|
||||||
const _navDiv = document.createElement('div')
|
|
||||||
_navDiv.className = 'nav'
|
|
||||||
_navDiv.append(_index, _close)
|
|
||||||
// gallery
|
|
||||||
const _gallery = document.createElement('div')
|
|
||||||
_gallery.className = 'gallery'
|
|
||||||
_gallery.append(_swiperNode)
|
|
||||||
_gallery.append(_navDiv)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* curtain
|
|
||||||
*/
|
|
||||||
const _curtain = document.createElement('div')
|
|
||||||
_curtain.className = 'curtain'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* container
|
|
||||||
* |- gallery
|
|
||||||
* |- curtain
|
|
||||||
*/
|
|
||||||
container.append(_gallery, _curtain)
|
|
||||||
}
|
|
||||||
193
assets/ts/mobile/gallery.tsx
Normal file
193
assets/ts/mobile/gallery.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { type gsap } from 'gsap'
|
||||||
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
on,
|
||||||
|
onMount,
|
||||||
|
untrack,
|
||||||
|
type JSX
|
||||||
|
} from 'solid-js'
|
||||||
|
import { createStore } from 'solid-js/store'
|
||||||
|
import { type Swiper } from 'swiper'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { loadGsap, removeDuplicates, type Vector } from '../utils'
|
||||||
|
|
||||||
|
import GalleryImage from './galleryImage'
|
||||||
|
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
||||||
|
import { closeGallery, openGallery } from './galleryTransitions'
|
||||||
|
import { getActiveImageIndexes, loadSwiper } from './galleryUtils'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
|
export default function Gallery(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
closeText: string
|
||||||
|
loadingText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
let _gsap: typeof gsap
|
||||||
|
let _swiper: Swiper | undefined
|
||||||
|
let initPromise: Promise<void> | undefined
|
||||||
|
|
||||||
|
let curtain: HTMLDivElement | undefined
|
||||||
|
let gallery: HTMLDivElement | undefined
|
||||||
|
let galleryInner: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const imageState = useImageState()
|
||||||
|
const [mobile, { setIndex, setIsAnimating, setIsScrollLocked }] = useMobileState()
|
||||||
|
|
||||||
|
const loadingText = createMemo(() => capitalizeFirstLetter(props.loadingText))
|
||||||
|
|
||||||
|
let lastIndex = -1
|
||||||
|
let mounted = false
|
||||||
|
let navigateVector: Vector = 'none'
|
||||||
|
|
||||||
|
const [libLoaded, setLibLoaded] = createSignal(false)
|
||||||
|
const [swiperReady, setSwiperReady] = createSignal(false)
|
||||||
|
const [loads, setLoads] = createStore(Array<boolean>(imageState().length).fill(false))
|
||||||
|
|
||||||
|
const slideUp: () => void = () => {
|
||||||
|
if (!libLoaded() || !mounted) return
|
||||||
|
|
||||||
|
invariant(curtain, 'curtain is not defined')
|
||||||
|
invariant(gallery, 'gallery is not defined')
|
||||||
|
|
||||||
|
openGallery({
|
||||||
|
gsap: _gsap,
|
||||||
|
curtain,
|
||||||
|
gallery,
|
||||||
|
setIsAnimating,
|
||||||
|
setIsScrollLocked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const slideDown: () => void = () => {
|
||||||
|
invariant(gallery, 'curtain is not defined')
|
||||||
|
invariant(curtain, 'gallery is not defined')
|
||||||
|
|
||||||
|
closeGallery({
|
||||||
|
gsap: _gsap,
|
||||||
|
curtain,
|
||||||
|
gallery,
|
||||||
|
setIsAnimating,
|
||||||
|
setIsScrollLocked,
|
||||||
|
onClosed: () => {
|
||||||
|
lastIndex = -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const galleryLoadImages: () => void = () => {
|
||||||
|
const currentIndex = mobile.index()
|
||||||
|
|
||||||
|
setLoads(
|
||||||
|
removeDuplicates(
|
||||||
|
getActiveImageIndexes(currentIndex, imageState().length, navigateVector)
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeSlide: (slide: number) => void = (slide) => {
|
||||||
|
if (!swiperReady() || _swiper === undefined) return
|
||||||
|
galleryLoadImages()
|
||||||
|
_swiper.slideTo(slide, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureGalleryReady: () => Promise<void> = async () => {
|
||||||
|
if (initPromise !== undefined) return await initPromise
|
||||||
|
|
||||||
|
initPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const [g, S] = await Promise.all([loadGsap(), loadSwiper()])
|
||||||
|
|
||||||
|
_gsap = g
|
||||||
|
|
||||||
|
invariant(galleryInner, 'galleryInner is not defined')
|
||||||
|
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||||
|
_swiper.on('slideChange', ({ realIndex }) => {
|
||||||
|
setIndex(realIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
setLibLoaded(true)
|
||||||
|
setSwiperReady(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
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => [swiperReady(), mobile.index()] as const,
|
||||||
|
([ready, index]) => {
|
||||||
|
if (!ready || index < 0) return
|
||||||
|
if (index === lastIndex) return
|
||||||
|
if (lastIndex === -1) navigateVector = 'none'
|
||||||
|
else if (index < lastIndex) navigateVector = 'prev'
|
||||||
|
else if (index > lastIndex) navigateVector = 'next'
|
||||||
|
else navigateVector = 'none'
|
||||||
|
changeSlide(index)
|
||||||
|
lastIndex = index
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => mobile.isOpen(),
|
||||||
|
async (isOpen) => {
|
||||||
|
if (isOpen && !swiperReady()) {
|
||||||
|
await ensureGalleryReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!libLoaded() || !swiperReady()) return
|
||||||
|
if (mobile.isAnimating()) return
|
||||||
|
if (isOpen) slideUp()
|
||||||
|
else slideDown()
|
||||||
|
},
|
||||||
|
{ defer: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={gallery} class="gallery">
|
||||||
|
<div ref={galleryInner} class="galleryInner">
|
||||||
|
<div class="swiper-wrapper">
|
||||||
|
<For each={imageState().images}>
|
||||||
|
{(ij, i) => (
|
||||||
|
<div class="swiper-slide">
|
||||||
|
<GalleryImage load={loads[i()]} ij={ij} loadingText={loadingText()} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GalleryNav closeText={props.closeText} />
|
||||||
|
</div>
|
||||||
|
<div ref={curtain} class="curtain" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
114
assets/ts/mobile/galleryImage.tsx
Normal file
114
assets/ts/mobile/galleryImage.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { type gsap } from 'gsap'
|
||||||
|
import { createEffect, on, onMount, type JSX } from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import type { ImageJSON } from '../resources'
|
||||||
|
import { loadGsap } from '../utils'
|
||||||
|
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
|
export default function GalleryImage(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
load: boolean
|
||||||
|
ij: ImageJSON
|
||||||
|
loadingText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
let img: HTMLImageElement | undefined
|
||||||
|
let loadingDiv: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
let _gsap: typeof gsap | undefined
|
||||||
|
let gsapPromise: Promise<typeof gsap> | undefined
|
||||||
|
let revealed = false
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
gsapPromise = loadGsap()
|
||||||
|
.then((g) => {
|
||||||
|
_gsap = g
|
||||||
|
return g
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
|
||||||
|
img?.addEventListener(
|
||||||
|
'load',
|
||||||
|
() => {
|
||||||
|
void revealImage()
|
||||||
|
},
|
||||||
|
{ 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 (
|
||||||
|
<>
|
||||||
|
<div class="slideContainer">
|
||||||
|
<img
|
||||||
|
ref={img}
|
||||||
|
{...(props.load && { src: props.ij.hiUrl })}
|
||||||
|
height={props.ij.hiImgH}
|
||||||
|
width={props.ij.hiImgW}
|
||||||
|
data-src={props.ij.hiUrl}
|
||||||
|
alt={props.ij.alt}
|
||||||
|
style={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
<div ref={loadingDiv} class="loadingText">
|
||||||
|
{props.loadingText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
54
assets/ts/mobile/galleryNav.tsx
Normal file
54
assets/ts/mobile/galleryNav.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { createMemo, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
import { expand } from '../utils'
|
||||||
|
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
|
export function capitalizeFirstLetter(str: string): string {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GalleryNav(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
closeText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
// states
|
||||||
|
const imageState = useImageState()
|
||||||
|
const [mobile, { setIsOpen }] = useMobileState()
|
||||||
|
const indexValue = createMemo(() => expand(mobile.index() + 1))
|
||||||
|
const indexLength = createMemo(() => expand(imageState().length))
|
||||||
|
|
||||||
|
const onClick: () => void = () => {
|
||||||
|
if (mobile.isAnimating()) return
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="nav">
|
||||||
|
<div>
|
||||||
|
<span class="num">{indexValue()[0]}</span>
|
||||||
|
<span class="num">{indexValue()[1]}</span>
|
||||||
|
<span class="num">{indexValue()[2]}</span>
|
||||||
|
<span class="num">{indexValue()[3]}</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span class="num">{indexLength()[0]}</span>
|
||||||
|
<span class="num">{indexLength()[1]}</span>
|
||||||
|
<span class="num">{indexLength()[2]}</span>
|
||||||
|
<span class="num">{indexLength()[3]}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="navClose"
|
||||||
|
onClick={onClick}
|
||||||
|
onTouchEnd={onClick}
|
||||||
|
onKeyDown={onClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
>
|
||||||
|
{capitalizeFirstLetter(props.closeText)}
|
||||||
|
</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 +0,0 @@
|
|||||||
import { type ImageJSON } from '../resources'
|
|
||||||
|
|
||||||
import { initCollection } from './collection'
|
|
||||||
import { initGallery } from './gallery'
|
|
||||||
|
|
||||||
export function initMobile(ijs: ImageJSON[]): void {
|
|
||||||
initCollection(ijs)
|
|
||||||
initGallery(ijs)
|
|
||||||
}
|
|
||||||
52
assets/ts/mobile/layout.tsx
Normal file
52
assets/ts/mobile/layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Show, createEffect, onCleanup, type JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { useImageState } from '../imageState'
|
||||||
|
|
||||||
|
import Collection from './collection'
|
||||||
|
import Gallery from './gallery'
|
||||||
|
import { useMobileState } from './state'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MobileImage extends HTMLImageElement {
|
||||||
|
dataset: {
|
||||||
|
src: string
|
||||||
|
index: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Mobile(props: {
|
||||||
|
children?: JSX.Element
|
||||||
|
closeText: string
|
||||||
|
loadingText: string
|
||||||
|
}): JSX.Element {
|
||||||
|
const imageState = useImageState()
|
||||||
|
const [mobile] = useMobileState()
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Show when={imageState().length > 0}>
|
||||||
|
<Collection />
|
||||||
|
<Gallery closeText={props.closeText} loadingText={props.loadingText} />
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,78 @@
|
|||||||
import { Watchable } from '../globalUtils'
|
import {
|
||||||
|
createComponent,
|
||||||
|
createContext,
|
||||||
|
createSignal,
|
||||||
|
useContext,
|
||||||
|
type Accessor,
|
||||||
|
type JSX,
|
||||||
|
type Setter
|
||||||
|
} from 'solid-js'
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
export const mounted = new Watchable<boolean>(false)
|
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,37 +0,0 @@
|
|||||||
import { type Swiper } from 'swiper'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* interfaces
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface MobileImage extends HTMLImageElement {
|
|
||||||
dataset: {
|
|
||||||
src: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function getRandom(min: number, max: number): number {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onIntersection<T extends HTMLElement>(
|
|
||||||
element: T,
|
|
||||||
callback: (arg0: IntersectionObserverEntry[], arg1: IntersectionObserver) => void
|
|
||||||
): void {
|
|
||||||
new IntersectionObserver((entries, observer) => {
|
|
||||||
callback(entries, observer)
|
|
||||||
}).observe(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function capitalizeFirstLetter(str: string): string {
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadSwiper(): Promise<typeof Swiper> {
|
|
||||||
const s = await import('swiper')
|
|
||||||
return s.Swiper
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { decThreshold, incThreshold, state } from './globalState'
|
|
||||||
import { expand } from './globalUtils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
// threshold div
|
|
||||||
const thresholdDiv = document
|
|
||||||
.getElementsByClassName('threshold')
|
|
||||||
.item(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[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function initNav(): void {
|
|
||||||
// add watcher for updating nav text
|
|
||||||
state.addWatcher((o) => {
|
|
||||||
updateIndexText(expand(o.index + 1), expand(o.length))
|
|
||||||
updateThresholdText(expand(o.threshold))
|
|
||||||
})
|
|
||||||
|
|
||||||
// event listeners
|
|
||||||
decButton.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
decThreshold()
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
incButton.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
incThreshold()
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper
|
|
||||||
|
|
||||||
export function updateThresholdText(thresholdValue: string): void {
|
|
||||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
|
||||||
e.innerText = thresholdValue[i]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -10,9 +10,20 @@ export interface ImageJSON {
|
|||||||
hiImgW: number
|
hiImgW: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initResources(): Promise<ImageJSON[]> {
|
export async function getImageJSON(): Promise<ImageJSON[]> {
|
||||||
|
if (document.title.split(' | ')[0] === '404') {
|
||||||
|
return [] // no images on 404 page
|
||||||
|
}
|
||||||
|
|
||||||
|
const ogUrlMetaTag = document.querySelector(
|
||||||
|
'meta[property="og:url"]'
|
||||||
|
) as HTMLMetaElement | null
|
||||||
|
const indexJsonUrl = ogUrlMetaTag?.content
|
||||||
|
? new URL('index.json', ogUrlMetaTag.content).href
|
||||||
|
: new URL('index.json', window.location.href).href
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${window.location.href}index.json`, {
|
const response = await fetch(indexJsonUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
}
|
}
|
||||||
@@ -24,7 +35,8 @@ export async function initResources(): Promise<ImageJSON[]> {
|
|||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { type Power3, type gsap } from 'gsap'
|
import { type gsap } from 'gsap'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Vector = 'prev' | 'next' | 'none'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utils
|
* utils
|
||||||
@@ -16,9 +22,9 @@ export function expand(num: number): string {
|
|||||||
return ('0000' + num.toString()).slice(-4)
|
return ('0000' + num.toString()).slice(-4)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadGsap(): Promise<[typeof gsap, typeof Power3]> {
|
export async function loadGsap(): Promise<typeof gsap> {
|
||||||
const g = await import('gsap')
|
const g = await import('gsap')
|
||||||
return [g.gsap, g.Power3]
|
return g.gsap
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThresholdSessionIndex(): number {
|
export function getThresholdSessionIndex(): number {
|
||||||
@@ -31,27 +37,3 @@ export function removeDuplicates<T>(arr: T[]): T[] {
|
|||||||
if (arr.length < 2) return arr // optimization
|
if (arr.length < 2) return arr // optimization
|
||||||
return [...new Set(arr)]
|
return [...new Set(arr)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* custom "reactive" object
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class Watchable<T> {
|
|
||||||
constructor(private obj: T) {}
|
|
||||||
private readonly watchers: Array<(arg0: T) => void> = []
|
|
||||||
|
|
||||||
get(): T {
|
|
||||||
return this.obj
|
|
||||||
}
|
|
||||||
|
|
||||||
set(e: T): void {
|
|
||||||
this.obj = e
|
|
||||||
this.watchers.forEach((watcher) => {
|
|
||||||
watcher(this.obj)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addWatcher(watcher: (arg0: T) => void): void {
|
|
||||||
this.watchers.push(watcher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
@@ -1,215 +0,0 @@
|
|||||||
# Getting Started
|
|
||||||
|
|
||||||
The files in `exampleSite` is just a simple example. Now, we will introduce it based on the same structure.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
Before you start, make sure you have installed Hugo **extended version**. For more information, see [Hugo's documentation](https://gohugo.io/getting-started/installing/).
|
|
||||||
|
|
||||||
Once you have installed Hugo, you can check the version by running the following command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hugo version
|
|
||||||
```
|
|
||||||
|
|
||||||
Which should output something like this (the version number may be different), notice the `extended` keyword:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hugo v0.120.3-a4892a07b41b7b3f1f143140ee4ec0a9a5cf3970+extended darwin/arm64 BuildDate=2023-11-01T17:57:00Z VendorInfo=brew
|
|
||||||
```
|
|
||||||
|
|
||||||
The minimum required Hugo version can be seen in the [`theme.toml`](https://github.com/Sped0n/bridget/blob/main/theme.toml#L19).
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Git (for adavanced user)
|
|
||||||
|
|
||||||
On the main branch, you can find the theme's latest source code. To use the latest version, you can clone the repository to `themes/bridget` by running the following command in the root directory of your Hugo site:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git clone https://github.com/Sped0n/bridget themes/bridget
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are already using Git for your site, you can add the theme as a submodule by running the following command in the root directory of your Hugo site:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git submodule add https://github.com/Sped0n/bridget themes/bridget
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️⚠️⚠️
|
|
||||||
>
|
|
||||||
> Please refer to the config section for the following content.
|
|
||||||
|
|
||||||
### Module (recommended)
|
|
||||||
|
|
||||||
> If you want to have some customizations, use Git installation instead.
|
|
||||||
|
|
||||||
This theme is also available as a [Hugo module](https://gohugo.io/hugo-modules/). Run the following command in the root directory of your Hugo site:
|
|
||||||
|
|
||||||
First turn your site into a Hugo module (in case you haven't done it yet):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hugo mod init github.com/me/my-new-site
|
|
||||||
# or whatever you like, it doesn’t necessarily have to be a GitHub repo link.
|
|
||||||
hugo mod init blablabla
|
|
||||||
```
|
|
||||||
|
|
||||||
Then import the theme as a dependency adding the following line to the `module` section of your site's configuration file.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# config/_default/hugo.toml
|
|
||||||
[module]
|
|
||||||
[[module.imports]]
|
|
||||||
path = "github.com/Sped0n/bridget"
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to upgrade the theme, just run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hugo mod get -u
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️⚠️⚠️
|
|
||||||
>
|
|
||||||
> Please refer to the config section for the following content.
|
|
||||||
|
|
||||||
## Content Management
|
|
||||||
|
|
||||||
The content is where the pictures/text is stored, while the static refers to the website icons.
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── content
|
|
||||||
│ ├── Erwitt
|
|
||||||
│ │ ├── 1.jpg
|
|
||||||
│ │ ├── ***
|
|
||||||
│ │ └── index.md
|
|
||||||
│ ├── Gruyaert
|
|
||||||
│ │ ├── 1.jpg
|
|
||||||
│ │ ├── ***
|
|
||||||
│ │ └── index.md
|
|
||||||
│ ├── Info
|
|
||||||
│ │ └── index.md
|
|
||||||
│ └── Webb
|
|
||||||
│ ├── 1.jpg
|
|
||||||
│ ├── ***
|
|
||||||
│ └── index.md
|
|
||||||
└── static
|
|
||||||
├── dot.png
|
|
||||||
└── dot.svg
|
|
||||||
```
|
|
||||||
|
|
||||||
In each index.md file, there is a configuration file like this:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
type: _default
|
|
||||||
layout: single
|
|
||||||
url: /erwitt/
|
|
||||||
menu:
|
|
||||||
main:
|
|
||||||
weight: 3
|
|
||||||
identifier: Erwitt
|
|
||||||
title: Erwitt
|
|
||||||
unifiedAlt: '© Elliott Erwitt'
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
- keep the `type` and `layout` **untouched**;
|
|
||||||
|
|
||||||
- `url` is the href link to this page, in this case, you can visit this page with `blabla.com/erwitt`;
|
|
||||||
|
|
||||||
- `main` is the entry to `menu`;
|
|
||||||
|
|
||||||
- `weight` determines the position of this link in the navigation bar, with the first one being 1, the second one being 2, and so on;
|
|
||||||
|
|
||||||
- `identifier` should be the **same** as the name of the **upper-level directory**;
|
|
||||||
|
|
||||||
- `title` refers to the text that appears on the navigation bar;
|
|
||||||
|
|
||||||
- `unifiedAlt` is **optional**, If you left it empty, the alt attribute of the image will default to its file name; if it is set, the alt attributes of all images will be unified to the value you have set;
|
|
||||||
|
|
||||||
- If this is a **showcase** page, simply place the images in the same directory as index.md.
|
|
||||||
|
|
||||||
- If this is an **information** page, you can continue writing the information you want to display in index.md.
|
|
||||||
|
|
||||||
> However, please note that the CSS for the information page **only provides simple styling for text**. If you have any requirements beyond text and the browser rendering does not meet your expectations, please modify [`_article.scss`](https://github.com/Sped0n/bridget/blob/main/assets/scss/_partial/_article.scss).
|
|
||||||
|
|
||||||
As for the **website icon**, place the files in static and then go to config part for further reading.
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
You can simply copy this to the root directory of your site with minor modifications, and you’ll be ready to proceed.
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
└── config
|
|
||||||
└── _default
|
|
||||||
├── hugo.toml
|
|
||||||
├── markup.toml
|
|
||||||
├── params.toml
|
|
||||||
└── sitemap.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### `hugo.toml`
|
|
||||||
|
|
||||||
We will focus on introducing the part about `theme as module`, detailed comments are provided for other options, so we won’t repeat them here.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# theme as module
|
|
||||||
[module]
|
|
||||||
replacements = "github.com/Sped0n/bridget -> ../.."
|
|
||||||
[[module.imports]]
|
|
||||||
path = "github.com/Sped0n/bridget"
|
|
||||||
```
|
|
||||||
|
|
||||||
- If you have <u>installation with Git</u>
|
|
||||||
|
|
||||||
- `replacement`: replace the <u>path after the arrow</u>(`../..`) with the location of your local theme file (⚠️⚠️⚠️**relative path only**, example: `themes/bridget`)
|
|
||||||
- `path`: no change
|
|
||||||
|
|
||||||
- If you have <u>installation with Module</u>, **remove the `replacements` configuration**.
|
|
||||||
|
|
||||||
### `markup.toml`
|
|
||||||
|
|
||||||
**DO NOT TOUCH THIS**
|
|
||||||
|
|
||||||
### `params.toml`
|
|
||||||
|
|
||||||
Detailed description in the comments.
|
|
||||||
|
|
||||||
> ⚠️⚠️⚠️
|
|
||||||
>
|
|
||||||
> Only thing that you need to pay **extra attention** is the [`bundled`](https://github.com/Sped0n/bridget/blob/1e2f1fadde9c16989eef1ab771f2ac8463dec5a4/exampleSite/config/_default/params.toml#L6) option, please read the corresponding doc and set it as your need.
|
|
||||||
>
|
|
||||||
> For users who have installation with module, please always set this option to `true`, unless you know what you are doing.
|
|
||||||
>
|
|
||||||
> Or you might get the error related to `node_modules/swiper/swiper.scss`.
|
|
||||||
|
|
||||||
### `sitemap.toml`
|
|
||||||
|
|
||||||
https://gohugo.io/templates/sitemap-template/#configuration
|
|
||||||
|
|
||||||
## Customization (AKA for developer)
|
|
||||||
|
|
||||||
> Before heading to this section, please make sure you have **installation with Git**.
|
|
||||||
|
|
||||||
### Option 1: _it just works_ way
|
|
||||||
|
|
||||||
> If you want to modify js/ts file, please use option 2.
|
|
||||||
|
|
||||||
1. Use hugo create a site and move the bridget theme into the theme directory.
|
|
||||||
2. Run `npm install` in the <u>bridget theme root dir</u>, not <u>your hugo site root dir</u>.
|
|
||||||
3. After the command is done, copy the `node_modules` dir from <u>bridget theme root dir</u> to <u>your hugo site root dir</u>.
|
|
||||||
4. In <u>your hugo site root dir</u>, write/modify configuration files according to your needs, remember to set `bundled` option to `false`, so hugo will not use prebuilt css file.
|
|
||||||
5. Run `hugo server` in <u>your hugo site root dir</u>, and you are good to go.
|
|
||||||
|
|
||||||
### Option 2: recommended way
|
|
||||||
|
|
||||||
1. Use hugo create a site and move the bridget theme into the theme directory.
|
|
||||||
2. Run `npm install` in the <u>bridget theme root dir</u>, not <u>your hugo site root dir</u>.
|
|
||||||
3. Run `npm run dev` in the <u>bridget theme root dir</u>, we will use content in exampleSite to debug.
|
|
||||||
4. Make your customization.
|
|
||||||
5. After modification, run `npm run build` in the <u>bridget theme root dir</u> to build artifacts.
|
|
||||||
6. In <u>your hugo site root dir</u>, write/modify configuration files according to your needs, remember to set `bundled` option to `true`, so hugo will use the artifacts you built in step 5.
|
|
||||||
7. Run `hugo server` in <u>your hugo site root dir</u>, and you are good to go.
|
|
||||||
307
docs.md
Normal file
307
docs.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
### Contents
|
||||||
|
|
||||||
|
- [Prequisites](#prequisites)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Hugo Modules (Recommended)](#hugo-modules-recommended)
|
||||||
|
- [Git Repository (For Customizations)](#git-repository-for-customizations)
|
||||||
|
- [Content Management](#content-management)
|
||||||
|
- [`index.md`](#indexmd)
|
||||||
|
- [Front Matter](#front-matter)
|
||||||
|
- [Markdown Content](#markdown-content)
|
||||||
|
- [Favicon](#favicon)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [`hugo.toml`](#hugotoml)
|
||||||
|
- [`markup.toml`](#markuptoml)
|
||||||
|
- [`outputs.toml`](#outputstoml)
|
||||||
|
- [`params.toml`](#paramstoml)
|
||||||
|
- [`sitemap.toml`](#sitemaptoml)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Customizations](#customizations)
|
||||||
|
- [Change Font](#change-font)
|
||||||
|
- [Add a Custom Analytic Script](#add-a-custom-analytic-script)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prequisites
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
- [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
|
||||||
|
❯ hugo version
|
||||||
|
hugo v0.152.2+extended+withdeploy darwin/arm64 BuildDate=unknown VendorInfo=nixpkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
- [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
|
||||||
|
❯ pnpm --version && node --version
|
||||||
|
10.20.0
|
||||||
|
v22.20.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
### Hugo Modules (Recommended)
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Checkout https://gohugo.io/hugo-modules/use-modules/#prerequisite before using Hugo Modules.
|
||||||
|
|
||||||
|
First turn your site into a Hugo module (in case you haven't done it yet):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hugo mod init github.com/me/my-new-site
|
||||||
|
# or whatever you like, it doesn’t need to be a valid GitHub repo link.
|
||||||
|
hugo mod init blablabla
|
||||||
|
```
|
||||||
|
|
||||||
|
Then import the theme as a dependency adding the following line to the `module` section of your site's configuration file.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# config/_default/hugo.toml
|
||||||
|
[module]
|
||||||
|
[[module.imports]]
|
||||||
|
path = "github.com/Sped0n/bridget/v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to upgrade the theme, just run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
hugo mod get -u
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Repository (For Customizations)
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
First clone the repository into your `themes` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# latest version (main branch, might be unstable)
|
||||||
|
git clone https://github.com/Sped0n/bridget themes/bridget
|
||||||
|
|
||||||
|
# and you can checkout to a specific stable version, see https://github.com/Sped0n/bridget/releases
|
||||||
|
cd themes/bridget
|
||||||
|
git checkout v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are already using Git for your site, you can add the theme as a submodule by running the following command in the root directory of your Hugo site:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add https://github.com/Sped0n/bridget themes/bridget
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content Management
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
The content is where the pictures/text is stored, while the static refers to the website icons.
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── content
|
||||||
|
│ ├── Erwitt
|
||||||
|
│ │ ├── 1.jpg
|
||||||
|
│ │ ├── ***
|
||||||
|
│ │ └── index.md
|
||||||
|
│ ├── Gruyaert
|
||||||
|
│ │ ├── 1.jpg
|
||||||
|
│ │ ├── ***
|
||||||
|
│ │ └── index.md
|
||||||
|
│ ├── Info
|
||||||
|
│ │ └── index.md
|
||||||
|
│ └── Webb
|
||||||
|
│ ├── 1.jpg
|
||||||
|
│ ├── ***
|
||||||
|
│ └── index.md
|
||||||
|
└── static
|
||||||
|
├── dot.png
|
||||||
|
└── dot.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
### `index.md`
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
#### Front Matter
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
Inside each index.md file, there is a front matter like this:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
type: _default # just copy
|
||||||
|
layout: single # just copy
|
||||||
|
url: /erwitt/
|
||||||
|
menu:
|
||||||
|
main:
|
||||||
|
weight: 3
|
||||||
|
identifier: Erwitt
|
||||||
|
title: Erwitt
|
||||||
|
unifiedAlt: '© Elliott Erwitt'
|
||||||
|
build:
|
||||||
|
publishResources: false # just copy
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- `url` is the href link to this page, in this case, you can visit this page with `blabla.com/erwitt`;
|
||||||
|
- `main` is the entry to `menu`;
|
||||||
|
- `weight` determines the position of this link in the navigation bar, with the first one being 1, the second one being 2, and so on;
|
||||||
|
- `identifier` should be the **same** as the name of the **upper-level directory**;
|
||||||
|
- `title` refers to the text that appears on the navigation bar;
|
||||||
|
- `unifiedAlt` is **optional**, If you left it empty, the alt attribute of the image will default to its file name; if it is set, the alt attributes of all images will be unified to the value you have set;
|
||||||
|
|
||||||
|
#### Markdown Content
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
- If this is a **showcase** page:
|
||||||
|
- No need to write anything in index.md.
|
||||||
|
- Place the images in the same directory as `index.md`.
|
||||||
|
- If this is an **information** page:
|
||||||
|
- You can write anything in index.md, and it will be rendered as HTML.
|
||||||
|
- However, please note that the CSS for the information page **only provides simple styling for text**. If you have any requirements beyond text and the browser rendering does not meet your expectations, please modify [`_article.scss`](https://github.com/Sped0n/bridget/blob/main/assets/scss/_partial/_article.scss).
|
||||||
|
|
||||||
|
### Favicon
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
As for the **website icon**, place the files under `static` directory and then go to [config](#configuration) part for further reading.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
You can simply copy `exampleSite/config` to the root directory, with some minor modifications and you should be good to go.
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
└── config
|
||||||
|
└── _default
|
||||||
|
├── hugo.toml
|
||||||
|
├── markup.toml
|
||||||
|
├── outputs.toml
|
||||||
|
├── params.toml
|
||||||
|
└── sitemap.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hugo.toml`
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
First, what you need to modify is the `baseURL` and `title`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# timeout
|
||||||
|
timeout = "1200s"
|
||||||
|
# your website url
|
||||||
|
baseURL = 'https://bridget-demo.sped0n.com' # <-- MODIFY ME
|
||||||
|
# website title
|
||||||
|
title = 'Bridget' # <-- MODIFY ME
|
||||||
|
# don't touch this
|
||||||
|
disableKinds = ["section", "taxonomy", "term", "home"]
|
||||||
|
# robots.txt
|
||||||
|
enableRobotsTXT = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Depend on which [installation](#installation) method you choose, you need to modify the `module` section:
|
||||||
|
|
||||||
|
- If you use [Hugo Modules](#hugo-modules-recommended):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[module]
|
||||||
|
[[module.imports]]
|
||||||
|
path = "github.com/Sped0n/bridget/v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you use [Git Repository](#git-repository-for-customizations):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[module]
|
||||||
|
# This is the relative path to hugo theme directory([official doc](https://gohugo.io/hugo-modules/configuration/#module-configuration-top-level))**.
|
||||||
|
replacements = "github.com/Sped0n/bridget/v2 -> ../.."
|
||||||
|
[[module.imports]]
|
||||||
|
path = "github.com/Sped0n/bridget/v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `markup.toml`
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
**Just copy it.**
|
||||||
|
|
||||||
|
### `outputs.toml`
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
**Just copy it.**
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
### `params.toml`
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
Detailed description in the comments.
|
||||||
|
|
||||||
|
### `sitemap.toml`
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Please make sure you have [installation with Git](#git-repository-for-customizations).
|
||||||
|
>
|
||||||
|
> If you want to try some changes on the `exampleSite`, below are some commands you might need:
|
||||||
|
>
|
||||||
|
> - `pnpm install` to install dependencies.
|
||||||
|
> - `pnpm run dev` to start a dev server (`http://localhost:1313`).
|
||||||
|
> - `pnpm run build` to update artifacts.
|
||||||
|
|
||||||
|
### Change Font
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
These are the places you need to focus on:
|
||||||
|
|
||||||
|
- `assets/scss/_core/_font.scss` (`@font-face`)
|
||||||
|
- `assets/scss/_core/_typography.scss` (`body.font-family`)
|
||||||
|
- `layouts/partials/head/link.html` (`preload`)
|
||||||
|
- `static/lib/fonts/GeistVF.woff2` (font file itself)
|
||||||
|
|
||||||
|
### Add a Custom Analytic Script
|
||||||
|
|
||||||
|
_[Contents](#contents)_
|
||||||
|
|
||||||
|
Go to `layouts/_default/baseof.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="{{- site.LanguageCode -}}">
|
||||||
|
<head>
|
||||||
|
/* ---------- INSERT HERE ---------- */
|
||||||
|
</head>
|
||||||
|
<body lang="{{- site.LanguageCode -}}">
|
||||||
|
<div class="analytics">/* ---------- OR HERE ---------- */</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
79
eslint.config.mjs
Normal file
79
eslint.config.mjs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import tsParser from '@typescript-eslint/parser'
|
||||||
|
import love from 'eslint-config-love'
|
||||||
|
import importPlugin from 'eslint-plugin-import'
|
||||||
|
import prettier from 'eslint-plugin-prettier/recommended'
|
||||||
|
import solid from 'eslint-plugin-solid/configs/recommended'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
importPlugin.flatConfigs.recommended,
|
||||||
|
solid,
|
||||||
|
globalIgnores(['node_modules/', 'static/', 'exampleSite/', '*.mjs', 'bundled/']),
|
||||||
|
{
|
||||||
|
...love,
|
||||||
|
...prettier,
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: './tsconfig.json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'arrow-body-style': 'off',
|
||||||
|
'prefer-arrow-callback': 'off',
|
||||||
|
'import/no-cycle': 'error',
|
||||||
|
|
||||||
|
'sort-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignoreCase: false,
|
||||||
|
ignoreDeclarationSort: true,
|
||||||
|
ignoreMemberSort: true,
|
||||||
|
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
|
||||||
|
allowSeparatedGroups: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
'import/no-unresolved': 'error',
|
||||||
|
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
'builtin',
|
||||||
|
'external',
|
||||||
|
'internal',
|
||||||
|
'parent',
|
||||||
|
'sibling',
|
||||||
|
'index',
|
||||||
|
'unknown'
|
||||||
|
],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
@@ -1,27 +1,16 @@
|
|||||||
# timeout
|
# timeout
|
||||||
timeout = "1200s"
|
timeout = "1200s"
|
||||||
# your website url
|
# your website url
|
||||||
baseURL = 'https://www.example.com/'
|
baseURL = 'https://bridget-demo.sped0n.com'
|
||||||
# website title
|
# website title
|
||||||
title = 'Bridget'
|
title = 'Bridget'
|
||||||
# don't touch this
|
# don't touch this
|
||||||
disableKinds = ["section", "taxonomy", "term", "home"]
|
disableKinds = ["section", "taxonomy", "term", "home"]
|
||||||
# robots.txt
|
# robots.txt
|
||||||
enableRobotsTXT = true
|
enableRobotsTXT = true
|
||||||
# available options
|
|
||||||
# * en (powered by Geist)
|
|
||||||
# * de (powered by Geist)
|
|
||||||
# * es (powered by Geist)
|
|
||||||
# * fr (powered by Geist)
|
|
||||||
# * it (powered by Geist)
|
|
||||||
# * zh-sg zh-cn (powered by Noto Sans SC)
|
|
||||||
# * zh-hk zh-tw zh-mo (powered by Noto Sans TC)
|
|
||||||
# * ja (powered by Noto Sans JP)
|
|
||||||
# * ko (powered by Noto Sans KR)
|
|
||||||
defaultContentLanguage = 'en'
|
|
||||||
|
|
||||||
# theme as module
|
# theme as module
|
||||||
[module]
|
[module]
|
||||||
replacements = "github.com/Sped0n/bridget -> ../.." # deploy with local 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" # 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)
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
|
# --- REQUIRED -----------------------------------------------------------------
|
||||||
|
|
||||||
# description of the site (will be placed in meta)
|
# description of the site (will be placed in meta)
|
||||||
description = "Bridget is a minimal Hugo theme designed for photographers/visual artists."
|
description = "Bridget is a minimal Hugo theme designed for photographers/visual artists."
|
||||||
# use bundled js and css
|
|
||||||
# * if you want to build the js and css from scratch, set this to false and run `npm install` and `npm run build`
|
# resize options for dynamic resolution, please refer to https://gohugo.io/content-management/image-processing/#image-processing-options
|
||||||
# * tldr: set this to false if you want to develop and edit the js and css
|
loResOpt = "800x webp Lanczos q60"
|
||||||
bundled = false
|
hiResOpt = "2500x webp Lanczos q75"
|
||||||
|
|
||||||
|
# labels (text shown on the UI)
|
||||||
|
[labels]
|
||||||
|
next = "next"
|
||||||
|
prev = "prev"
|
||||||
|
close = "close"
|
||||||
|
threshold = "Threshold"
|
||||||
|
error = "page not found"
|
||||||
|
loading = "loading..."
|
||||||
|
|
||||||
|
# --- OPTIONAL -----------------------------------------------------------------
|
||||||
|
|
||||||
# whether to use favicon resource links
|
# whether to use favicon resource links
|
||||||
# generate these with https://realfavicongenerator.net
|
# generate these with https://realfavicongenerator.net
|
||||||
@@ -13,16 +26,7 @@ svgFavicon = "/dot.svg"
|
|||||||
# fallback png favicon for unsupported browsers
|
# fallback png favicon for unsupported browsers
|
||||||
svgFaviconFallback = "/dot.png"
|
svgFaviconFallback = "/dot.png"
|
||||||
|
|
||||||
# resize options for dynamic resolution, please refer to https://gohugo.io/content-management/image-processing/#image-processing-options
|
# site verification code for Google/Bing/Yandex/Pinterest/Baidu
|
||||||
loResOpt = "800x webp Lanczos q60"
|
|
||||||
hiResOpt = "2500x webp Lanczos q75"
|
|
||||||
|
|
||||||
# page config
|
|
||||||
[page]
|
|
||||||
# unified alt text for all images in the page
|
|
||||||
unifiedAlt = ''
|
|
||||||
|
|
||||||
# Site verification code for Google/Bing/Yandex/Pinterest/Baidu
|
|
||||||
[verification]
|
[verification]
|
||||||
google = ""
|
google = ""
|
||||||
bing = ""
|
bing = ""
|
||||||
@@ -32,7 +36,7 @@ baidu = ""
|
|||||||
so = ""
|
so = ""
|
||||||
sogou = ""
|
sogou = ""
|
||||||
|
|
||||||
# Analytics config
|
# analytics config
|
||||||
[analytics]
|
[analytics]
|
||||||
enable = true
|
enable = true
|
||||||
# Google Analytics
|
# Google Analytics
|
||||||
@@ -50,10 +54,10 @@ server = ""
|
|||||||
id = ""
|
id = ""
|
||||||
# Umami Analytics
|
# Umami Analytics
|
||||||
[analytics.umami]
|
[analytics.umami]
|
||||||
data_website_id = "44a4a42d-ec8e-44c9-a38c-7533929e9845"
|
data_website_id = "942d4c0d-ebd0-4da7-936a-bd278af32e5e"
|
||||||
src = "https://umami.sped0nwen.com/script.js"
|
src = "https://umami.sped0n.com/script.js"
|
||||||
data_host_url = ""
|
data_host_url = ""
|
||||||
data_domains = "bridget-demo.sped0nwen.com"
|
data_domains = "bridget-demo.sped0n.com"
|
||||||
# Plausible Analytics
|
# Plausible Analytics
|
||||||
[analytics.plausible]
|
[analytics.plausible]
|
||||||
data_domain = ""
|
data_domain = ""
|
||||||
@@ -65,8 +69,8 @@ token = ""
|
|||||||
[analytics.splitbee]
|
[analytics.splitbee]
|
||||||
enable = false
|
enable = false
|
||||||
# no cookie mode
|
# no cookie mode
|
||||||
No_cookie = true
|
no_cookie = true
|
||||||
# respect the do not track setting of the browser
|
# respect the do not track setting of the browser
|
||||||
Do_not_track = true
|
do_not_track = true
|
||||||
# token(optional), more info on https://splitbee.io/docs/embed-the-script
|
# token(optional), more info on https://splitbee.io/docs/embed-the-script
|
||||||
data_token = ""
|
data_token = ""
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ menu:
|
|||||||
identifier: Erwitt
|
identifier: Erwitt
|
||||||
title: Erwitt
|
title: Erwitt
|
||||||
unifiedAlt: '© Elliott Erwitt'
|
unifiedAlt: '© Elliott Erwitt'
|
||||||
|
build:
|
||||||
|
publishResources: false
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ menu:
|
|||||||
identifier: Gruyaert
|
identifier: Gruyaert
|
||||||
title: Gruyaert
|
title: Gruyaert
|
||||||
unifiedAlt: '© Harry Gruyaert'
|
unifiedAlt: '© Harry Gruyaert'
|
||||||
|
build:
|
||||||
|
publishResources: false
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ menu:
|
|||||||
identifier: Info
|
identifier: Info
|
||||||
title: Info
|
title: Info
|
||||||
unifiedAlt: ''
|
unifiedAlt: ''
|
||||||
|
build:
|
||||||
|
publishResources: false
|
||||||
---
|
---
|
||||||
|
|
||||||
Bridget is a _minimal_ Hugo theme designed for photographers/visual artists.
|
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 raw TypeScript and CSS. 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 components, 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_.
|
||||||
|
|
||||||
Once again, great shout out to <u>[Tyler McRobert](https://tylermcrobert.com)</u> for his inspiration to this project.
|
Once again, great shout out to <u>[Tyler McRobert](https://tylermcrobert.com)</u> for his inspiration to this project.
|
||||||
|
|
||||||
[Repo ↗](https://github.com/Sped0n/bridget)
|
[GitHub Repo ↗](https://github.com/Sped0n/bridget)
|
||||||
|
|
||||||
Original site design by <u>[Tyler McRobert](https://tylermcrobert.com)</u>.
|
Original site design by <u>[Tyler McRobert](https://tylermcrobert.com)</u>.
|
||||||
|
|
||||||
© {{< year >}} <u>[Spedon](https://github.com/Sped0n)</u> | Powered by [Hugo](https://gohugo.io)
|
© {{< year >}} <u>[Spedon](https://github.com/Sped0n)</u> | Built with Hugo
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ menu:
|
|||||||
identifier: Webb
|
identifier: Webb
|
||||||
title: Webb
|
title: Webb
|
||||||
unifiedAlt: '© Alex Webb'
|
unifiedAlt: '© Alex Webb'
|
||||||
|
build:
|
||||||
|
publishResources: false
|
||||||
---
|
---
|
||||||
|
|||||||
25
flake.lock
generated
Normal file
25
flake.lock
generated
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769461804,
|
||||||
|
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||||
|
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||||
|
"revCount": 935279,
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.935279%2Brev-bfc1b8a4574108ceef22f02bafcf6611380c100d/019c02ef-f13d-717e-8527-f1603ec205db/source.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
37
flake.nix
Normal file
37
flake.nix
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
description = "bridget";
|
||||||
|
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1";
|
||||||
|
outputs =
|
||||||
|
{ self, ... }@inputs:
|
||||||
|
let
|
||||||
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forEachSupportedSystem =
|
||||||
|
f:
|
||||||
|
inputs.nixpkgs.lib.genAttrs supportedSystems (
|
||||||
|
system:
|
||||||
|
f {
|
||||||
|
pkgs = import inputs.nixpkgs { inherit system; };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forEachSupportedSystem (
|
||||||
|
{ pkgs }:
|
||||||
|
{
|
||||||
|
default = pkgs.mkShellNoCC {
|
||||||
|
packages = with pkgs; [
|
||||||
|
nodejs
|
||||||
|
nodePackages.pnpm
|
||||||
|
hugo
|
||||||
|
go
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -1,3 +1,3 @@
|
|||||||
module github.com/Sped0n/bridget
|
module github.com/Sped0n/bridget/v2
|
||||||
|
|
||||||
go 1.21.3
|
go 1.21.3
|
||||||
|
|||||||
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"
|
||||||
12
i18n/de.toml
12
i18n/de.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "nächste"
|
|
||||||
[prev]
|
|
||||||
other = "vorher"
|
|
||||||
[close]
|
|
||||||
other = "schließen"
|
|
||||||
[threshold]
|
|
||||||
other = "schwelle"
|
|
||||||
[404]
|
|
||||||
other = "seite nicht gefunden"
|
|
||||||
[loading]
|
|
||||||
other = "lade"
|
|
||||||
12
i18n/en.toml
12
i18n/en.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "next"
|
|
||||||
[prev]
|
|
||||||
other = "prev"
|
|
||||||
[close]
|
|
||||||
other = "close"
|
|
||||||
[threshold]
|
|
||||||
other = "threshold"
|
|
||||||
[404]
|
|
||||||
other = "page not found"
|
|
||||||
[loading]
|
|
||||||
other = "loading"
|
|
||||||
12
i18n/es.toml
12
i18n/es.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "siguiente"
|
|
||||||
[prev]
|
|
||||||
other = "previo"
|
|
||||||
[close]
|
|
||||||
other = "cerrar"
|
|
||||||
[threshold]
|
|
||||||
other = "umbral"
|
|
||||||
[404]
|
|
||||||
other = "página no encontrada"
|
|
||||||
[loading]
|
|
||||||
other = "cargando"
|
|
||||||
12
i18n/fr.toml
12
i18n/fr.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "suivant"
|
|
||||||
[prev]
|
|
||||||
other = "précédent"
|
|
||||||
[close]
|
|
||||||
other = "fermer"
|
|
||||||
[threshold]
|
|
||||||
other = "seuil"
|
|
||||||
[404]
|
|
||||||
other = "page non trouvée"
|
|
||||||
[loading]
|
|
||||||
other = "chargement"
|
|
||||||
12
i18n/it.toml
12
i18n/it.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "prossimo"
|
|
||||||
[prev]
|
|
||||||
other = "precedente"
|
|
||||||
[close]
|
|
||||||
other = "chiudi"
|
|
||||||
[threshold]
|
|
||||||
other = "soglia"
|
|
||||||
[404]
|
|
||||||
other = "pagina non trovata"
|
|
||||||
[loading]
|
|
||||||
other = "caricamento"
|
|
||||||
12
i18n/ja.toml
12
i18n/ja.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "進む"
|
|
||||||
[prev]
|
|
||||||
other = "後退"
|
|
||||||
[close]
|
|
||||||
other = "閉じる"
|
|
||||||
[threshold]
|
|
||||||
other = "しきい値"
|
|
||||||
[404]
|
|
||||||
other = "ページが見つかりません"
|
|
||||||
[loading]
|
|
||||||
other = "読み込み中"
|
|
||||||
12
i18n/ko.toml
12
i18n/ko.toml
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "전진"
|
|
||||||
[prev]
|
|
||||||
other = "물러나세요"
|
|
||||||
[close]
|
|
||||||
other = "닫기"
|
|
||||||
[threshold]
|
|
||||||
other = "임계값"
|
|
||||||
[404]
|
|
||||||
other = "페이지를 찾을 수 없습니다"
|
|
||||||
[loading]
|
|
||||||
other = "로딩중"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "前进"
|
|
||||||
[prev]
|
|
||||||
other = "后退"
|
|
||||||
[close]
|
|
||||||
other = "关闭"
|
|
||||||
[threshold]
|
|
||||||
other = "阈值"
|
|
||||||
[404]
|
|
||||||
other = "页面不存在"
|
|
||||||
[loading]
|
|
||||||
other = "加载中"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "前進"
|
|
||||||
[prev]
|
|
||||||
other = "後退"
|
|
||||||
[close]
|
|
||||||
other = "關閉"
|
|
||||||
[threshold]
|
|
||||||
other = "閾值"
|
|
||||||
[404]
|
|
||||||
other = "找不到頁面"
|
|
||||||
[loading]
|
|
||||||
other = "載入中"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "前進"
|
|
||||||
[prev]
|
|
||||||
other = "後退"
|
|
||||||
[close]
|
|
||||||
other = "關閉"
|
|
||||||
[threshold]
|
|
||||||
other = "閾值"
|
|
||||||
[404]
|
|
||||||
other = "找不到頁面"
|
|
||||||
[loading]
|
|
||||||
other = "載入中"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "前进"
|
|
||||||
[prev]
|
|
||||||
other = "后退"
|
|
||||||
[close]
|
|
||||||
other = "关闭"
|
|
||||||
[threshold]
|
|
||||||
other = "阈值"
|
|
||||||
[404]
|
|
||||||
other = "页面不存在"
|
|
||||||
[loading]
|
|
||||||
other = "加载中"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[next]
|
|
||||||
other = "前進"
|
|
||||||
[prev]
|
|
||||||
other = "後退"
|
|
||||||
[close]
|
|
||||||
other = "關閉"
|
|
||||||
[threshold]
|
|
||||||
other = "閾值"
|
|
||||||
[404]
|
|
||||||
other = "找不到頁面"
|
|
||||||
[loading]
|
|
||||||
other = "載入中"
|
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{- partial "nav.html" . -}}
|
{{- partial "nav.html" . -}}
|
||||||
|
<article>
|
||||||
|
<p class="error">
|
||||||
|
⛝ <u>404</u> {{- site.Params.labels.error -}} ⛝
|
||||||
|
</p>
|
||||||
|
<p class="error">
|
||||||
|
⛝ <u>404</u> {{- site.Params.labels.error -}} ⛝
|
||||||
|
</p>
|
||||||
|
<p class="error">
|
||||||
|
⛝ <u>404</u> {{- site.Params.labels.error -}} ⛝
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<article class="info">
|
|
||||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
|
||||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
|
||||||
<p class="error">⛝ <u>404</u> {{- i18n 404 -}} ⛝</p>
|
|
||||||
</article>
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="container"
|
||||||
data-next="{{- i18n "next" -}}"
|
data-next="{{- site.Params.labels.next -}}"
|
||||||
data-prev="{{- i18n "prev" -}}"
|
data-prev="{{- site.Params.labels.prev -}}"
|
||||||
data-close="{{- i18n "close" -}}"
|
data-close="{{- site.Params.labels.close -}}"
|
||||||
data-loading="{{- i18n "loading" -}}"
|
data-loading="{{- site.Params.labels.loading -}}"
|
||||||
>
|
>
|
||||||
{{- partial "nav.html" . -}}
|
|
||||||
{{- with .Content -}}
|
{{- with .Content -}}
|
||||||
<article class="info">
|
<article>
|
||||||
{{- . -}}
|
{{- . -}}
|
||||||
</article>
|
</article>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- partial "nav.html" . -}}
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user