initial-commit
This commit is contained in:
commit
c679e0b967
42 changed files with 6275 additions and 0 deletions
0
.env
Normal file
0
.env
Normal file
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
24
|
||||
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# nicwands.com
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
13
index.html
Normal file
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
41
package copy.json
Normal file
41
package copy.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "nicwands.com",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite-ssg build",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fuzzco/font-loader": "^1.0.2",
|
||||
"@unhead/vue": "^2.0.19",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"gsap": "^3.13.0",
|
||||
"lenis": "^1.3.15",
|
||||
"lodash": "^4.17.21",
|
||||
"sass": "^1.94.2",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"tempus": "^1.0.0-dev.17",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite-ssg": "^28.2.2",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-vue-devtools": "^8.0.3"
|
||||
}
|
||||
}
|
||||
4482
package-lock.json
generated
Normal file
4482
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
41
package.json
Normal file
41
package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "nicwands.com",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite-ssg build",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fuzzco/font-loader": "^1.0.2",
|
||||
"@unhead/vue": "^2.0.19",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"gsap": "^3.13.0",
|
||||
"lenis": "^1.3.15",
|
||||
"lodash": "^4.17.21",
|
||||
"sass": "^1.94.2",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"tempus": "^1.0.0-dev.17",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite-ssg": "^28.2.2",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-vue-devtools": "^8.0.3"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
public/fonts/OfficeTimesRound-Regular.woff
Executable file
BIN
public/fonts/OfficeTimesRound-Regular.woff
Executable file
Binary file not shown.
BIN
public/fonts/OfficeTimesRound-Regular.woff2
Executable file
BIN
public/fonts/OfficeTimesRound-Regular.woff2
Executable file
Binary file not shown.
59
src/App.vue
Normal file
59
src/App.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<lenis root :options="{ duration: 1 }">
|
||||
<div :class="classes" :style="styles">
|
||||
<suspense>
|
||||
<layout />
|
||||
</suspense>
|
||||
</div>
|
||||
</lenis>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import loadFonts from '@fuzzco/font-loader'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
const { height } = useWindowSize()
|
||||
|
||||
const classes = computed(() => [
|
||||
'container',
|
||||
{ 'fonts-ready': !fontsLoading.value },
|
||||
'theme-light',
|
||||
])
|
||||
|
||||
const fontsLoading = ref(true)
|
||||
onMounted(async () => {
|
||||
// Load fonts
|
||||
loadFonts([
|
||||
{
|
||||
name: 'Office Times',
|
||||
weights: [400],
|
||||
},
|
||||
])
|
||||
.then(() => {
|
||||
fontsLoading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
fontsLoading.value = false
|
||||
})
|
||||
})
|
||||
|
||||
const styles = computed(() => ({
|
||||
'--vh': height.value ? height.value / 100 + 'px' : '100vh',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
min-height: calc(100 * var(--vh));
|
||||
max-width: 100vw;
|
||||
overflow-x: clip;
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-fg);
|
||||
transition: opacity 1000ms;
|
||||
|
||||
&:not(.fonts-ready) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
src/components.d.ts
vendored
Normal file
20
src/components.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Layout: typeof import('./components/Layout.vue')['default']
|
||||
Lenis: typeof import('./components/Lenis.vue')['default']
|
||||
LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
20
src/components/Layout.vue
Normal file
20
src/components/Layout.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="layout">
|
||||
<h1>nicwands.com</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSeoMeta } from '@unhead/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
// SEO
|
||||
// const seo = computed(() => content.value?.seo?.[0])
|
||||
// useSeoMeta({
|
||||
// title: seo.value?.title || '',
|
||||
// description: seo.value.description || '',
|
||||
// ogImage: {
|
||||
// url: seo.value.og_image?.filename || '',
|
||||
// },
|
||||
// })
|
||||
</script>
|
||||
69
src/components/Lenis.vue
Normal file
69
src/components/Lenis.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div v-if="!root" class="lenis" ref="wrapper">
|
||||
<div ref="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot v-else />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Lenis from 'lenis'
|
||||
import Tempus from 'tempus'
|
||||
import { onBeforeUnmount, onMounted, provide, ref, shallowRef } from 'vue'
|
||||
|
||||
const { root, instance, options } = defineProps({
|
||||
root: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
instance: { type: String },
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
duration: 1.2,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const lenis = shallowRef()
|
||||
const wrapper = ref()
|
||||
const content = ref()
|
||||
const removeRaf = ref()
|
||||
|
||||
// Provide instance for useLenis composable
|
||||
const instanceKey = `lenis${instance ? `-${instance}` : ''}`
|
||||
provide(instanceKey, lenis)
|
||||
|
||||
// Initialize with Tempus
|
||||
const initLenis = () => {
|
||||
if (lenis.value) return
|
||||
|
||||
lenis.value = new Lenis({
|
||||
...options,
|
||||
...(!root
|
||||
? {
|
||||
wrapper: wrapper.value,
|
||||
content: content.value,
|
||||
eventsTarget: wrapper.value,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
removeRaf.value = Tempus.add((time) => {
|
||||
lenis.value.raf(time)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!lenis.value) {
|
||||
initLenis()
|
||||
}
|
||||
})
|
||||
// Kill lenis before unmount
|
||||
onBeforeUnmount(() => {
|
||||
lenis.value?.destroy()
|
||||
removeRaf.value?.()
|
||||
})
|
||||
</script>
|
||||
32
src/components/LoadingSpinner.vue
Normal file
32
src/components/LoadingSpinner.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<svg
|
||||
class="loading-spinner"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
style="margin: auto; display: block"
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
stroke-width="4"
|
||||
stroke="currentColor"
|
||||
stroke-dasharray="62 62"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
repeatCount="indefinite"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="0 50 50;360 50 50"
|
||||
></animateTransform>
|
||||
</circle>
|
||||
</svg>
|
||||
</template>
|
||||
14
src/composables/useLenis.js
Normal file
14
src/composables/useLenis.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { inject, onBeforeUnmount } from 'vue'
|
||||
|
||||
export default (callback = () => {}, instanceId) => {
|
||||
const instanceKey = `lenis${instanceId ? `-${instanceId}` : ''}`
|
||||
const lenis = inject(instanceKey)
|
||||
|
||||
if (lenis.value) {
|
||||
lenis.value.on('scroll', callback)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => lenis.value?.off('scroll', callback))
|
||||
|
||||
return lenis
|
||||
}
|
||||
29
src/composables/useRelativeSize.js
Normal file
29
src/composables/useRelativeSize.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useWindowSize } from '@vueuse/core'
|
||||
import { viewports } from '@/libs/theme'
|
||||
|
||||
const { width: wWidth, height: wHeight } = useWindowSize()
|
||||
|
||||
export default () => {
|
||||
// Desktop
|
||||
const dvw = (pixels) => {
|
||||
return (pixels / viewports.desktop.width) * wWidth.value
|
||||
}
|
||||
const dvh = (pixels) => {
|
||||
return (pixels / viewports.desktop.height) * wHeight.value
|
||||
}
|
||||
|
||||
// Mobile
|
||||
const mvw = (pixels) => {
|
||||
return (pixels / viewports.mobile.width) * wWidth.value
|
||||
}
|
||||
const mvh = (pixels) => {
|
||||
return (pixels / viewports.mobile.height) * wHeight.value
|
||||
}
|
||||
|
||||
return {
|
||||
dvw,
|
||||
dvh,
|
||||
mvw,
|
||||
mvh,
|
||||
}
|
||||
}
|
||||
46
src/composables/useScrollProgress.js
Normal file
46
src/composables/useScrollProgress.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
useElementBounding,
|
||||
useIntersectionObserver,
|
||||
useWindowSize,
|
||||
} from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import { mapRange, clamp } from '@/libs/math'
|
||||
import useLenis from '@/composables/useLenis'
|
||||
|
||||
const { height: wHeight } = useWindowSize()
|
||||
|
||||
export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
|
||||
const isActive = ref(true)
|
||||
const smoothProgress = ref(0)
|
||||
|
||||
const { height, top } = useElementBounding(el)
|
||||
|
||||
const isIntersected = ref(false)
|
||||
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
|
||||
isIntersected.value = isIntersecting
|
||||
})
|
||||
|
||||
useLenis(({ scroll }) => {
|
||||
if (!isActive.value) return
|
||||
if (!height.value || !wHeight.value) return
|
||||
if (!isIntersected.value) return
|
||||
|
||||
const pageTop = scroll + top.value
|
||||
|
||||
const start = pageTop - wHeight.value * entry
|
||||
const end = pageTop + height.value - wHeight.value * exit
|
||||
|
||||
let rawProgress = mapRange(start, end, scroll, 0, 1)
|
||||
rawProgress = clamp(0, rawProgress, 1)
|
||||
|
||||
smoothProgress.value += (rawProgress - smoothProgress.value) * 0.1
|
||||
callback?.(smoothProgress.value)
|
||||
})
|
||||
|
||||
const destroy = () => {
|
||||
isActive.value = false
|
||||
stop?.()
|
||||
}
|
||||
|
||||
return { destroy }
|
||||
}
|
||||
89
src/composables/useSmoothMouse.js
Normal file
89
src/composables/useSmoothMouse.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import gsap from 'gsap'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useWindowSize, useEventListener } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
* Shared global raw mouse state (only set up once)
|
||||
*/
|
||||
const rawMouse = {
|
||||
x: ref(0),
|
||||
y: ref(0),
|
||||
initialized: false,
|
||||
cleanup: null,
|
||||
}
|
||||
|
||||
const useGlobalMouseListener = () => {
|
||||
if (!rawMouse.initialized && !import.meta.env.SSR) {
|
||||
rawMouse.initialized = true
|
||||
rawMouse.cleanup = useEventListener(window, 'mousemove', (e) => {
|
||||
rawMouse.x.value = e.clientX
|
||||
rawMouse.y.value = e.clientY
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for smoothed mouse position with customizable smoothing and normalization.
|
||||
*/
|
||||
export default (options) => {
|
||||
const smoothFactor = options?.smoothFactor ?? 0.1
|
||||
const normalize = options?.normalize ?? false
|
||||
const callback = options?.onUpdate
|
||||
|
||||
const { width: wWidth, height: wHeight } = useWindowSize()
|
||||
|
||||
const sx = ref(0)
|
||||
const sy = ref(0)
|
||||
|
||||
useGlobalMouseListener()
|
||||
|
||||
const getTargetX = () =>
|
||||
normalize ? rawMouse.x.value / wWidth.value : rawMouse.x.value
|
||||
const getTargetY = () =>
|
||||
normalize ? rawMouse.y.value / wHeight.value : rawMouse.y.value
|
||||
|
||||
let tween
|
||||
onMounted(() => {
|
||||
if (tween) tween.kill
|
||||
|
||||
// Start smoothing tween
|
||||
tween = gsap.to(
|
||||
{ x: sx.value, y: sy.value },
|
||||
{
|
||||
duration: 1,
|
||||
ease: 'linear',
|
||||
repeat: -1,
|
||||
onUpdate: () => {
|
||||
const newX = gsap.utils.interpolate(
|
||||
sx.value,
|
||||
getTargetX(),
|
||||
smoothFactor,
|
||||
)
|
||||
const newY = gsap.utils.interpolate(
|
||||
sy.value,
|
||||
getTargetY(),
|
||||
smoothFactor,
|
||||
)
|
||||
|
||||
sx.value = newX
|
||||
sy.value = newY
|
||||
|
||||
callback?.({ sx: sx.value, sy: sy.value })
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
tween?.kill()
|
||||
rawMouse.cleanup()
|
||||
rawMouse.initialized = false
|
||||
})
|
||||
|
||||
return {
|
||||
sx,
|
||||
sy,
|
||||
rawX: rawMouse.x,
|
||||
rawY: rawMouse.y,
|
||||
}
|
||||
}
|
||||
34
src/composables/useSubmitForm.js
Normal file
34
src/composables/useSubmitForm.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { ref } from 'vue'
|
||||
|
||||
export default (url) => {
|
||||
const loading = ref(false)
|
||||
const success = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const submit = async (headers = {}, body = {}) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: new URLSearchParams(body),
|
||||
})
|
||||
|
||||
success.value = true
|
||||
error.value = ''
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
error.value = err.toString().split('Error: ')[1]
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
success,
|
||||
error,
|
||||
submit,
|
||||
}
|
||||
}
|
||||
37
src/composables/useTimelineTransition.js
Normal file
37
src/composables/useTimelineTransition.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import gsap from 'gsap'
|
||||
|
||||
export default (timelineSetup = () => {}) => {
|
||||
const timelines = new WeakMap()
|
||||
const getTimeline = (el) => {
|
||||
if (!timelines.has(el)) {
|
||||
const tl = gsap.timeline({ paused: true })
|
||||
|
||||
timelines.set(el, tl)
|
||||
}
|
||||
return timelines.get(el)
|
||||
}
|
||||
const onEnter = (el, done) => {
|
||||
const tl = getTimeline(el)
|
||||
|
||||
tl.clear()
|
||||
timelineSetup?.(tl, el)
|
||||
|
||||
tl.eventCallback('onReverseComplete', null)
|
||||
tl.eventCallback('onComplete', done)
|
||||
tl.play()
|
||||
}
|
||||
const onLeave = (el, done) => {
|
||||
el.style.pointerEvents = 'none'
|
||||
|
||||
const tl = getTimeline(el)
|
||||
|
||||
tl.eventCallback('onComplete', null)
|
||||
tl.eventCallback('onReverseComplete', done)
|
||||
tl.reverse()
|
||||
}
|
||||
|
||||
return {
|
||||
onEnter,
|
||||
onLeave,
|
||||
}
|
||||
}
|
||||
22
src/libs/math.js
Normal file
22
src/libs/math.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export function clamp(min, input, max) {
|
||||
return Math.max(min, Math.min(input, max))
|
||||
}
|
||||
|
||||
export function mapRange(in_min, in_max, input, out_min, out_max) {
|
||||
return (
|
||||
((input - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
|
||||
)
|
||||
}
|
||||
|
||||
export function lerp(start, end, amt) {
|
||||
return (1 - amt) * start + amt * end
|
||||
}
|
||||
|
||||
export function truncate(value, decimals) {
|
||||
return parseFloat(value.toFixed(decimals))
|
||||
}
|
||||
|
||||
export function wrapValue(value, lowBound, highBound) {
|
||||
const range = highBound - lowBound
|
||||
return ((((value - lowBound) % range) + range) % range) + lowBound
|
||||
}
|
||||
220
src/libs/sass-utils/index.js
Normal file
220
src/libs/sass-utils/index.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// https://github.com/dawaltconley/sass-cast/blob/main/index.js
|
||||
|
||||
import * as sass from 'sass-embedded'
|
||||
import { isQuoted, unquoteString, parseString, getAttr } from './utils'
|
||||
import { List, OrderedMap } from 'immutable'
|
||||
|
||||
/**
|
||||
* Converts any Javascript object to an equivalent Sass value.
|
||||
*
|
||||
* This method is recursive and will convert the values of any array or object,
|
||||
* as well as the array or object itself.
|
||||
*
|
||||
* @example
|
||||
* const { toSass } = require('sass-cast');
|
||||
*
|
||||
* const string = toSass('a simple string');
|
||||
* // quoted SassString => '"a simple string"'
|
||||
*
|
||||
* const map = toSass({
|
||||
* key: 'value',
|
||||
* nested: {
|
||||
* 'complex//:key': [ null, 4 ],
|
||||
* }
|
||||
* });
|
||||
* // SassMap => '("key": "value", "nested": ("complex//:key": (null, 4)))'
|
||||
*
|
||||
* @param {*} value - the value to be converted
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.parseUnquotedStrings=false] - whether to parse unquoted strings for colors or numbers with units
|
||||
* @param {boolean|*[]} [options.resolveFunctions=false] - if true, resolve functions and attempt to cast their return values. if an array, pass as arguments when resolving
|
||||
* @param {boolean} [options.quotes=true] - controls whether returned SassStrings are quoted. input strings that contain quotes will always return a quoted SassString even if this flag is false.
|
||||
* @return {Value} - a {@link https://sass-lang.com/documentation/js-api/classes/Value Sass value}
|
||||
*/
|
||||
|
||||
export const toSass = (value, options = {}) => {
|
||||
let {
|
||||
parseUnquotedStrings = false,
|
||||
resolveFunctions = false,
|
||||
quotes = true,
|
||||
} = options
|
||||
if (value instanceof sass.Value) {
|
||||
return value
|
||||
} else if (value === null || value === undefined) {
|
||||
return sass.sassNull
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? sass.sassTrue : sass.sassFalse
|
||||
} else if (typeof value === 'number') {
|
||||
return new sass.SassNumber(value)
|
||||
} else if (typeof value === 'string') {
|
||||
const valueIsQuoted = isQuoted(value)
|
||||
if (parseUnquotedStrings && !valueIsQuoted) {
|
||||
let parsed = parseString(value)
|
||||
if (
|
||||
parsed instanceof sass.SassColor ||
|
||||
parsed instanceof sass.SassNumber
|
||||
)
|
||||
return parsed
|
||||
}
|
||||
return new sass.SassString(value, {
|
||||
quotes: valueIsQuoted || quotes,
|
||||
})
|
||||
} else if (typeof value === 'object') {
|
||||
if (Array.isArray(value)) {
|
||||
let sassList = value.map((value) => toSass(value, options))
|
||||
return new sass.SassList(sassList)
|
||||
} else {
|
||||
let sassMap = OrderedMap(value).mapEntries(([key, value]) => [
|
||||
new sass.SassString(key, { quotes: true }),
|
||||
toSass(value, options),
|
||||
])
|
||||
return new sass.SassMap(sassMap)
|
||||
}
|
||||
} else if (resolveFunctions && typeof value === 'function') {
|
||||
const args = Array.isArray(resolveFunctions) ? resolveFunctions : []
|
||||
return toSass(value(...args), options)
|
||||
}
|
||||
return sass.sassNull
|
||||
}
|
||||
|
||||
const colorProperties = [
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'hue',
|
||||
'lightness',
|
||||
'saturation',
|
||||
'whiteness',
|
||||
'blackness',
|
||||
'alpha',
|
||||
]
|
||||
|
||||
/**
|
||||
* Converts Sass values to their Javascript equivalents.
|
||||
*
|
||||
* @example
|
||||
* const { fromSass, toSass } = require('sass-cast');
|
||||
*
|
||||
* const sassString = toSass('a sass string object');
|
||||
* const string = fromSass(sassString);
|
||||
* // 'a sass string object'
|
||||
*
|
||||
* @param {Value} object - a {@link https://sass-lang.com/documentation/js-api/classes/Value Sass value}
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.preserveUnits=false] - By default, only the values of numbers are returned, not their units. If true, `fromSass` will return numbers as a two-item Array, i.e. [ value, unit ]
|
||||
* @param {boolean} [options.rgbColors=false] - By default, colors are returned as strings. If true, `fromSass` will return colors as an object with `r`, `g`, `b`, and `a`, properties.
|
||||
* @param {boolean} [options.preserveQuotes=false] - By default, quoted Sass strings return their inner text as a string. If true, `fromSass` will preserve the quotes in the returned string value.
|
||||
* @return {*} - a Javascript value corresponding to the Sass input
|
||||
*/
|
||||
|
||||
export const fromSass = (object, options = {}) => {
|
||||
let {
|
||||
preserveUnits = false,
|
||||
rgbColors = false,
|
||||
preserveQuotes = false,
|
||||
} = options
|
||||
if (object instanceof sass.SassBoolean) {
|
||||
return object.value
|
||||
} else if (object instanceof sass.SassNumber) {
|
||||
if (preserveUnits) {
|
||||
return [
|
||||
object.value,
|
||||
object.numeratorUnits.toArray(),
|
||||
object.denominatorUnits.toArray(),
|
||||
]
|
||||
} else if (object.numeratorUnits.size || object.denominatorUnits.size) {
|
||||
return object.toString()
|
||||
}
|
||||
return object.value
|
||||
} else if (object instanceof sass.SassColor) {
|
||||
if (rgbColors) {
|
||||
return colorProperties.reduce((colorObj, p) => {
|
||||
colorObj[p] = object[p]
|
||||
return colorObj
|
||||
}, {})
|
||||
}
|
||||
return object.toString()
|
||||
} else if (object instanceof sass.SassString) {
|
||||
return preserveQuotes ? object.text : unquoteString(object.text)
|
||||
} else if (object instanceof sass.SassList || List.isList(object)) {
|
||||
let list = []
|
||||
for (
|
||||
let i = 0, value = object.get(i);
|
||||
value !== undefined;
|
||||
i++, value = object.get(i)
|
||||
) {
|
||||
list.push(fromSass(value, options))
|
||||
}
|
||||
return list
|
||||
} else if (object instanceof sass.SassMap) {
|
||||
return object.contents
|
||||
.mapEntries(([k, v]) => [k.text, fromSass(v, options)])
|
||||
.toObject()
|
||||
} else {
|
||||
return object.realNull
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An object defining Sass utility functions.
|
||||
*
|
||||
* @example <caption>Pass to sass using the JS API</caption>
|
||||
* const { sassFunctions } = require('sass-cast');
|
||||
* const sass = require('sass');
|
||||
*
|
||||
* sass.compile('main.scss', { functions: sassFunctions });
|
||||
*/
|
||||
export const sassFunctions = {
|
||||
/**
|
||||
* Sass function for importing data from Javascript or JSON files.
|
||||
* Calls the CommonJS `require` function under the hood.
|
||||
*
|
||||
* #### Examples
|
||||
*
|
||||
* ```scss
|
||||
* // import config info from tailwindcss
|
||||
* $tw: require('./tailwind.config.js', $parseUnquotedStrings: true);
|
||||
* $tw-colors: map.get($tw, theme, extend, colors);
|
||||
* ```
|
||||
* @name require
|
||||
* @memberof sassFunctions
|
||||
* @param {SassString} $module - Path to the file or module. Relative paths are relative to the Node process running Sass compilation.
|
||||
* @param {SassList} [$properties=()] - List of properties, if you only want to parse part of the module data.
|
||||
* @param {SassBoolean} [$parseUnquotedStrings=false] - Passed as an option to {@link #tosass toSass}.
|
||||
* @param {SassBoolean} [$resolveFunctions=false] - Passed as an option to {@link #tosass toSass}.
|
||||
* @param {SassBoolean} [$quotes=true] - Passed as an option to {@link #tosass toSass}.
|
||||
* @return {Value} - a {@link https://sass-lang.com/documentation/js-api/classes/Value Sass value}
|
||||
*/
|
||||
'require($module, $properties: (), $parseUnquotedStrings: false, $resolveFunctions: false, $quotes: true)':
|
||||
(args) => {
|
||||
const moduleName = args[0].assertString('module').text
|
||||
const properties = args[1].realNull && fromSass(args[1].asList)
|
||||
const parseUnquotedStrings = args[2].isTruthy
|
||||
const resolveFunctions = args[3].isTruthy
|
||||
const quotes = args[4].isTruthy
|
||||
const options = {
|
||||
parseUnquotedStrings,
|
||||
resolveFunctions,
|
||||
quotes,
|
||||
}
|
||||
const convert = (data) =>
|
||||
toSass(properties ? getAttr(data, properties) : data, options)
|
||||
|
||||
let mod,
|
||||
paths = [moduleName, `${process.cwd()}/${moduleName}`]
|
||||
for (let path of paths) {
|
||||
try {
|
||||
mod = require(path)
|
||||
break
|
||||
} catch (e) {
|
||||
if (e.code !== 'MODULE_NOT_FOUND') throw e
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (!mod) throw new Error(`Couldn't find module: ${moduleName}`)
|
||||
|
||||
if (resolveFunctions && typeof mod === 'function') mod = mod()
|
||||
if (mod instanceof Promise) return mod.then(convert)
|
||||
return convert(mod)
|
||||
},
|
||||
}
|
||||
108
src/libs/sass-utils/utils.js
Normal file
108
src/libs/sass-utils/utils.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// https://github.com/dawaltconley/sass-cast/blob/main/utils.js
|
||||
|
||||
import * as sass from 'sass-embedded'
|
||||
|
||||
/**
|
||||
* Check if string is quoted
|
||||
* @private
|
||||
* @param {string} str
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isQuoted = (str) => /^['"].*['"]$/.test(str)
|
||||
|
||||
/**
|
||||
* Surrounds a string with quotes
|
||||
* @private
|
||||
* @param {string} str
|
||||
* @param {string} q - quotes, double or single
|
||||
* @return {string}
|
||||
*/
|
||||
export const quoteString = (str, q) => {
|
||||
if (!q) return str
|
||||
if (isQuoted(str)) {
|
||||
q = str[0]
|
||||
str = str.slice(1, -1)
|
||||
}
|
||||
let r = new RegExp(q, 'g')
|
||||
return q + str.replace(r, '\\' + q) + q
|
||||
}
|
||||
|
||||
/**
|
||||
* Unquotes a string
|
||||
* @private
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
*/
|
||||
export const unquoteString = (str) => (isQuoted(str) ? str.slice(1, -1) : str)
|
||||
|
||||
/**
|
||||
* Parse a string as a Sass object
|
||||
* cribbed from davidkpiano/sassport
|
||||
*
|
||||
* @private
|
||||
* @param {string} str
|
||||
* @return {Value}
|
||||
*/
|
||||
export const parseString = (str) => {
|
||||
let result
|
||||
|
||||
try {
|
||||
sass.compileString(`$_: ___(${str});`, {
|
||||
functions: {
|
||||
'___($value)': (args) => {
|
||||
result = args[0]
|
||||
return result
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
return str
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string as a legacy Sass object
|
||||
* cribbed from davidkpiano/sassport
|
||||
*
|
||||
* @private
|
||||
* @param {string} str
|
||||
* @return {LegacyObject}
|
||||
*/
|
||||
export const parseStringLegacy = (str) => {
|
||||
let result
|
||||
|
||||
try {
|
||||
sass.renderSync({
|
||||
data: `$_: ___((${str}));`,
|
||||
functions: {
|
||||
'___($value)': (value) => {
|
||||
result = value
|
||||
return value
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
return str
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle 'toString()' methods with legacy API.
|
||||
*
|
||||
* @private
|
||||
* @param {LegacyObject} obj
|
||||
* @return {string}
|
||||
*/
|
||||
export const legacyToString = (obj) => (obj.dartValue || obj).toString()
|
||||
|
||||
/**
|
||||
* Return a value from an object and a list of keys.
|
||||
* @private
|
||||
* @param {Object|Array} obj
|
||||
* @param {*[]} attrs
|
||||
*/
|
||||
export const getAttr = (obj, attrs) => attrs.reduce((o, attr) => o[attr], obj)
|
||||
43
src/libs/theme.js
Normal file
43
src/libs/theme.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const colors = {
|
||||
black: '#000000',
|
||||
white: '#d9d9d9',
|
||||
pink: '#DAC3C3',
|
||||
green: '#D8E3D3',
|
||||
cream: '#F2F1F1',
|
||||
grey: '#2B2B2B',
|
||||
}
|
||||
|
||||
const themes = {
|
||||
light: {
|
||||
bg: colors.white,
|
||||
fg: colors.black,
|
||||
},
|
||||
dark: {
|
||||
bg: colors.black,
|
||||
fg: colors.white,
|
||||
},
|
||||
}
|
||||
|
||||
const breakpoints = {
|
||||
mobile: 800,
|
||||
}
|
||||
|
||||
const viewports = {
|
||||
mobile: {
|
||||
width: 440,
|
||||
height: 956,
|
||||
},
|
||||
desktop: {
|
||||
width: 1728,
|
||||
height: 1117,
|
||||
},
|
||||
}
|
||||
|
||||
export { colors, themes, breakpoints, viewports }
|
||||
|
||||
export default {
|
||||
colors,
|
||||
themes,
|
||||
breakpoints,
|
||||
viewports,
|
||||
}
|
||||
7
src/main.js
Normal file
7
src/main.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import './styles/main.scss'
|
||||
import App from './App.vue'
|
||||
import { ViteSSG } from 'vite-ssg/single-page'
|
||||
|
||||
export const createApp = ViteSSG(App, ({ app }) => {
|
||||
// Plugins
|
||||
})
|
||||
7
src/styles/_colors.scss
Normal file
7
src/styles/_colors.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
@use 'sass:color';
|
||||
|
||||
:root {
|
||||
@each $name, $color in getColors() {
|
||||
--#{$name}: #{$color};
|
||||
}
|
||||
}
|
||||
22
src/styles/_easings.scss
Normal file
22
src/styles/_easings.scss
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
:root {
|
||||
--ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
|
||||
--ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
--ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22);
|
||||
--ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||
--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
--ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335);
|
||||
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
--ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
||||
--ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
--ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
--ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
|
||||
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
|
||||
--ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||
|
||||
--ease-custom: cubic-bezier(0.315, 0.365, 0.23, 0.985);
|
||||
}
|
||||
31
src/styles/_font-style.scss
Normal file
31
src/styles/_font-style.scss
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@use 'functions' as *;
|
||||
|
||||
@mixin size-font($ds, $ms) {
|
||||
font-size: mobile-vw($ms);
|
||||
|
||||
&.vh {
|
||||
font-size: mobile-vh($ms);
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
font-size: desktop-vw($ds);
|
||||
|
||||
&.vh {
|
||||
font-size: desktop-vh($ds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin h1 {
|
||||
font-family: var(--font-times);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.1em;
|
||||
@include size-font(42px, 27px);
|
||||
}
|
||||
|
||||
@mixin p {
|
||||
font-family: var(--font-times);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.03em;
|
||||
@include size-font(25px, 18px);
|
||||
}
|
||||
26
src/styles/_fonts.scss
Normal file
26
src/styles/_fonts.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Font Weights:
|
||||
100 - Thin
|
||||
200 - Extra Light (Ultra Light)
|
||||
300 - Light
|
||||
400 - Normal
|
||||
500 - Medium
|
||||
600 - Semi Bold (Demi Bold)
|
||||
700 - Bold
|
||||
800 - Extra Bold (Ultra Bold)
|
||||
900 - Black (Heavy)
|
||||
*/
|
||||
|
||||
/* OFFICE TIMES */
|
||||
@font-face {
|
||||
font-family: 'Office Times';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
url('/fonts/OfficeTimesRound-Regular.woff2') format('woff2'),
|
||||
url('/fonts/OfficeTimesRound-Regular.woff') format('woff');
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-times: 'Office Times', monospace;
|
||||
}
|
||||
187
src/styles/_functions.scss
Normal file
187
src/styles/_functions.scss
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
@use 'sass:math';
|
||||
|
||||
/* Breakpoints */
|
||||
$mobile-breakpoint: get('breakpoints.mobile');
|
||||
|
||||
// Viewport Sizes
|
||||
$desktop-width: get('viewports.desktop.width');
|
||||
$desktop-height: get('viewports.desktop.height');
|
||||
|
||||
$mobile-width: get('viewports.mobile.width');
|
||||
$mobile-height: get('viewports.mobile.height');
|
||||
|
||||
// Breakpoint
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$mobile-breakpoint * 1px - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$mobile-breakpoint * 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@function mobile-vw($pixels, $base-vw: $mobile-width) {
|
||||
$px: math.div($pixels, $base-vw);
|
||||
$perc: math.div($px, 1px);
|
||||
@return calc($perc * 100vw);
|
||||
}
|
||||
|
||||
@function mobile-vh($pixels, $base-vh: $mobile-height) {
|
||||
$px: math.div($pixels, $base-vh);
|
||||
$perc: math.div($px, 1px);
|
||||
@return calc($perc * 100vh);
|
||||
}
|
||||
|
||||
@function desktop-vw($pixels, $base-vw: $desktop-width) {
|
||||
$px: math.div($pixels, $base-vw);
|
||||
$perc: math.div($px, 1px);
|
||||
@return calc($perc * 100vw);
|
||||
}
|
||||
|
||||
@function desktop-vh($pixels, $base-vh: $desktop-height) {
|
||||
$px: math.div($pixels, $base-vh);
|
||||
$perc: math.div($px, 1px);
|
||||
@return calc($perc * 100vh);
|
||||
}
|
||||
|
||||
@function columns($columns) {
|
||||
@return calc(
|
||||
(#{$columns} * var(--layout-column-width)) +
|
||||
((#{$columns} - 1) * var(--layout-column-gap))
|
||||
);
|
||||
}
|
||||
|
||||
@mixin child-grid($columns) {
|
||||
grid-template-columns: repeat($columns, minmax(0, 1fr));
|
||||
column-gap: var(--layout-columns-gap);
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@mixin reduced-motion {
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fill($position: absolute) {
|
||||
position: #{$position};
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@mixin fade-on-ready($class: 'ready', $duration: 400ms) {
|
||||
opacity: 0;
|
||||
transition: opacity $duration ease;
|
||||
|
||||
&.#{$class} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp text block to number of lines
|
||||
@mixin line-clamp($lines: 3, $mobile-lines: $lines) {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: $lines;
|
||||
|
||||
@include mobile {
|
||||
-webkit-line-clamp: $mobile-lines;
|
||||
}
|
||||
}
|
||||
|
||||
// Flip animations
|
||||
@keyframes flip-r {
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
51% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes flip-l {
|
||||
50% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
51% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes flip-d {
|
||||
50% {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
51% {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes flip-u {
|
||||
50% {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
51% {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin flip-animation(
|
||||
$direction: 'r',
|
||||
$duration: 600ms,
|
||||
$easing: var(--ease-out-expo),
|
||||
$iteration-count: 1
|
||||
) {
|
||||
overflow: hidden;
|
||||
animation: flip-#{$direction} $duration $easing $iteration-count forwards;
|
||||
}
|
||||
|
||||
@mixin link-hover {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: var(--theme-fg);
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
transition: transform 300ms var(--ease-out-quad);
|
||||
}
|
||||
@include desktop {
|
||||
&:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin stagger-animate($stagger: 100, $num-children: 10, $base-delay: 0) {
|
||||
@for $i from 0 through $num-children {
|
||||
&:nth-child(#{$i + 1}) {
|
||||
transition-delay: #{$stagger * $i + $base-delay}ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hover {
|
||||
@include desktop {
|
||||
&:hover {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/styles/_layers.scss
Normal file
7
src/styles/_layers.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// z-index
|
||||
.lily-cursor {
|
||||
z-index: 20;
|
||||
}
|
||||
.site-header {
|
||||
z-index: 10;
|
||||
}
|
||||
91
src/styles/_layout.scss
Normal file
91
src/styles/_layout.scss
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
@use 'sass:list';
|
||||
@use 'functions' as *;
|
||||
|
||||
// css variables exposed globally:
|
||||
// --layout-column-count: columns count in the layout
|
||||
// --layout-column-gap: gap size between columns
|
||||
// --layout-margin: layout margin size (left or right)
|
||||
// --layout-width: 100vw minus 2 * --layout-margin
|
||||
// --layout-column-width: size of a single column
|
||||
|
||||
// css classes exposed globally:
|
||||
// .layout-block: element takes the whole layout width
|
||||
// .layout-block-inner: same as .layout-block but using padding instead of margin
|
||||
// .layout-grid: extends .layout-block with grid behaviour using layout settings
|
||||
// .layout-grid-inner: same as .layout-grid but using padding instead of margin
|
||||
|
||||
@use 'sass:map';
|
||||
|
||||
// config to fill
|
||||
// 'variable': (mobile, desktop)
|
||||
$layout: (
|
||||
'columns-count': (
|
||||
5,
|
||||
18,
|
||||
),
|
||||
'columns-gap': (
|
||||
20px,
|
||||
20px,
|
||||
),
|
||||
'margin': (
|
||||
30px,
|
||||
60px,
|
||||
),
|
||||
);
|
||||
|
||||
//internal process, do not touch
|
||||
:root {
|
||||
--layout-column-count: #{list.nth(map.get($layout, 'columns-count'), 1)};
|
||||
--layout-column-gap: #{mobile-vw(
|
||||
list.nth(map.get($layout, 'columns-gap'), 1)
|
||||
)};
|
||||
--layout-margin: #{mobile-vw(list.nth(map.get($layout, 'margin'), 1))};
|
||||
--layout-width: calc(100vw - (2 * var(--layout-margin)));
|
||||
--layout-column-width: calc(
|
||||
(
|
||||
var(--layout-width) -
|
||||
(
|
||||
(var(--layout-column-count) - 1) *
|
||||
var(--layout-column-gap)
|
||||
)
|
||||
) /
|
||||
var(--layout-column-count)
|
||||
);
|
||||
|
||||
@include desktop {
|
||||
--layout-column-count: #{list.nth(map.get($layout, 'columns-count'), 2)};
|
||||
--layout-column-gap: #{desktop-vw(
|
||||
list.nth(map.get($layout, 'columns-gap'), 2)
|
||||
)};
|
||||
--layout-margin: #{desktop-vw(list.nth(map.get($layout, 'margin'), 2))};
|
||||
}
|
||||
}
|
||||
|
||||
.layout-block {
|
||||
max-width: var(--layout-width);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-block-inner {
|
||||
padding-left: var(--layout-margin);
|
||||
padding-right: var(--layout-margin);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
@extend .layout-block;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
||||
grid-gap: var(--layout-column-gap);
|
||||
}
|
||||
|
||||
.layout-grid-inner {
|
||||
@extend .layout-block-inner;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
||||
grid-gap: var(--layout-column-gap);
|
||||
}
|
||||
99
src/styles/_reset.scss
Normal file
99
src/styles/_reset.scss
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/***
|
||||
The new CSS reset - version 1.9 (last updated 19.6.2023)
|
||||
GitHub page: https://github.com/elad2412/the-new-css-reset
|
||||
***/
|
||||
|
||||
/*
|
||||
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
||||
- The "symbol *" part is to solve Firefox SVG sprite bug
|
||||
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
|
||||
*/
|
||||
*:where(
|
||||
:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)
|
||||
) {
|
||||
all: unset;
|
||||
display: revert;
|
||||
}
|
||||
|
||||
/* Preferred box-sizing value */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Reapply the pointer cursor for anchor tags */
|
||||
a,
|
||||
button {
|
||||
cursor: revert;
|
||||
}
|
||||
|
||||
/* For images to not be able to exceed their container */
|
||||
img {
|
||||
max-inline-size: 100%;
|
||||
max-block-size: 100%;
|
||||
}
|
||||
|
||||
/* removes spacing between cells in tables */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* revert the 'white-space' property for textarea elements on Safari */
|
||||
textarea {
|
||||
white-space: revert;
|
||||
}
|
||||
|
||||
/* minimum style to allow to style meter element */
|
||||
meter {
|
||||
-webkit-appearance: revert;
|
||||
appearance: revert;
|
||||
}
|
||||
|
||||
/* preformatted text - use only for this feature */
|
||||
:where(pre) {
|
||||
all: revert;
|
||||
}
|
||||
|
||||
/* reset default text opacity of input placeholder */
|
||||
::placeholder {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
/* remove default dot (•) sign */
|
||||
::marker {
|
||||
content: initial;
|
||||
}
|
||||
|
||||
/* fix the feature of 'hidden' attribute.
|
||||
display:revert; revert to element instead of attribute */
|
||||
:where([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* revert for bug in Chromium browsers
|
||||
- fix for the content editable attribute will work properly.
|
||||
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
||||
: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;
|
||||
}
|
||||
|
||||
/* apply back the draggable feature - exist only in Chromium and Safari */
|
||||
:where([draggable='true']) {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
/* Revert Modal native behavior */
|
||||
:where(dialog:modal) {
|
||||
all: revert;
|
||||
}
|
||||
34
src/styles/_scroll.scss
Normal file
34
src/styles/_scroll.scss
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
html {
|
||||
&:not(.dev) {
|
||||
&,
|
||||
* {
|
||||
scrollbar-width: none !important;
|
||||
-ms-overflow-style: none !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.lenis {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.lenis.lenis-smooth {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.lenis.lenis-stopped {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lenis.lenis-scrolling iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
11
src/styles/_themes.scss
Normal file
11
src/styles/_themes.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
@use 'sass:color';
|
||||
|
||||
:root {
|
||||
@each $name, $theme in getThemes() {
|
||||
.theme-#{$name} {
|
||||
@each $name, $color in $theme {
|
||||
--theme-#{$name}: #{$color};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/styles/_transitions.scss
Normal file
43
src/styles/_transitions.scss
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Fades
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 400ms;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.quick-fade-enter-active,
|
||||
.quick-fade-leave-active {
|
||||
transition: opacity 100ms;
|
||||
}
|
||||
.quick-fade-enter-from,
|
||||
.quick-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.slow-fade-enter-active,
|
||||
.slow-fade-leave-active {
|
||||
transition: opacity 600ms;
|
||||
}
|
||||
.slow-fade-enter-from,
|
||||
.slow-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Slides
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: transform 400ms var(--ease-out-quad);
|
||||
}
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active {
|
||||
transition: transform 400ms var(--ease-out-quad);
|
||||
}
|
||||
.slide-left-enter-from,
|
||||
.slide-left-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
42
src/styles/_utils.scss
Normal file
42
src/styles/_utils.scss
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
@use 'functions' as *;
|
||||
|
||||
.full-width {
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
@include desktop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
@include mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.has-scroll-smooth) {
|
||||
.hide-on-native-scroll {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html.has-scroll-smooth {
|
||||
.hide-on-smooth-scroll {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
111
src/styles/main.scss
Normal file
111
src/styles/main.scss
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
@use 'colors' as *;
|
||||
@use 'themes' as *;
|
||||
@use 'easings' as *;
|
||||
@use 'reset' as *;
|
||||
@use 'layers' as *;
|
||||
@use 'functions' as *;
|
||||
@use 'utils' as *;
|
||||
@use 'fonts' as *;
|
||||
@use 'font-style' as *;
|
||||
@use 'layout' as *;
|
||||
@use 'scroll' as *;
|
||||
@use 'transitions' as *;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// Type
|
||||
.h1,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@include h1;
|
||||
}
|
||||
.p,
|
||||
p,
|
||||
a,
|
||||
button,
|
||||
input,
|
||||
pre {
|
||||
@include p;
|
||||
}
|
||||
|
||||
.entry {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
p {
|
||||
min-height: 1px;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
& > * {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
& > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
& > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
li {
|
||||
margin-bottom: 1em;
|
||||
width: desktop-vw(577px);
|
||||
}
|
||||
}
|
||||
ul {
|
||||
list-style: disc;
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
5% {
|
||||
opacity: 0;
|
||||
}
|
||||
40% {
|
||||
opacity: 0;
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bonk {
|
||||
0% {
|
||||
transform: rotate(calc(var(--bonk-angle) * -1));
|
||||
}
|
||||
50% {
|
||||
transform: rotate(var(--bonk-angle));
|
||||
}
|
||||
}
|
||||
55
vite.config.js
Normal file
55
vite.config.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { toSass } from './src/libs/sass-utils'
|
||||
import theme from './src/libs/theme'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
Components({
|
||||
dirs: ['src/components'],
|
||||
extensions: ['vue'],
|
||||
deep: true,
|
||||
dts: 'src/components.d.ts',
|
||||
directoryAsNamespace: true,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
additionalData:
|
||||
'@use "/src/styles/_functions.scss" as *; @use "/src/styles/_font-style.scss" as *;',
|
||||
functions: {
|
||||
'get($keys)': function (keys) {
|
||||
keys = keys.toString().replace(/['"]+/g, '').split('.')
|
||||
|
||||
let result = theme
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
result = result[keys[i]]
|
||||
}
|
||||
|
||||
return toSass(result)
|
||||
},
|
||||
'getColors()': function () {
|
||||
return toSass(theme.colors)
|
||||
},
|
||||
'getThemes()': function () {
|
||||
return toSass(theme.themes)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in a new issue