initial-commit

This commit is contained in:
nicwands
2026-02-13 17:56:58 -05:00
commit a4fb3f2a87
60 changed files with 16775 additions and 0 deletions

74
src/main/index.js Normal file
View File

@@ -0,0 +1,74 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

20
src/preload/index.js Normal file
View File

@@ -0,0 +1,20 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.api = api
}

17
src/renderer/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

65
src/renderer/src/App.vue Normal file
View File

@@ -0,0 +1,65 @@
<template>
<lenis root :options="{ duration: 1 }">
<div :class="classes" :style="styles">
<main class="layout-block">
<h1>Taker of Notes</h1>
<p>Lorem ipsum</p>
</main>
</div>
</lenis>
</template>
<script setup>
import Lenis from './components/Lenis.vue'
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-dark',
])
const fontsLoading = ref(true)
onMounted(async () => {
// Load fonts
loadFonts([
{
name: 'Leibniz Fraktur',
weights: [400],
},
{
name: 'Geist Mono',
weights: [400, 700],
},
])
.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>

View 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>

View File

@@ -0,0 +1,59 @@
import { gsap } from 'gsap'
import { onMounted, onBeforeUnmount, watch, unref } from 'vue'
/**
* Infinite up/down floating animation for an element.
*
* @param {Ref|HTMLElement} target - element or ref to animate
* @param {Object} opts
* @param {number} opts.y - distance to move up/down
* @param {number} opts.duration - duration of one cycle (up or down)
* @param {number} opts.delay - initial delay
* @param {string} opts.ease - GSAP easing
*/
export default (
target,
{ y = '-3%', duration = 2, delay = 0, ease = 'power1.inOut' } = {},
) => {
let tl = null
const start = () => {
const el = unref(target)
if (!el) return
// Kill any previous timeline
if (tl) tl.kill()
// Create infinite yoyo animation
tl = gsap.to(el, {
y: y,
duration,
delay,
ease,
yoyo: true,
repeat: -1,
})
}
const stop = () => {
if (tl) {
tl.kill()
tl = null
}
}
onMounted(() => {
// If the element is a ref, wait for it to appear
watch(
() => unref(target),
(el) => {
if (el) start()
},
{ immediate: true },
)
})
onBeforeUnmount(stop)
return { start, stop }
}

View 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
}

View 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,
}
}

View 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 }
}

View 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,
}
}

View 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,
}
}

View 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,
}
}

View 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
}

View 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)
},
}

View 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)

View 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,
}

5
src/renderer/src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import './styles/main.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@@ -0,0 +1,7 @@
@use 'sass:color';
:root {
@each $name, $color in getColors() {
--#{$name}: #{$color};
}
}

View 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);
}

View 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-display);
font-weight: 400;
letter-spacing: 0.1em;
@include size-font(42px, 27px);
}
@mixin p {
font-family: var(--font-mono);
font-weight: 400;
letter-spacing: 0.03em;
@include size-font(25px, 18px);
}

View File

@@ -0,0 +1,45 @@
/*
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)
*/
/* Leibniz Fraktur */
@font-face {
font-family: 'Leibniz Fraktur';
font-style: normal;
font-weight: 400;
src:
url('../../../../resources/fonts/leibniz-fraktur.woff2') format('woff2'),
url('../../../../resources/fonts/leibniz-fraktur.woff') format('woff');
}
/* Geist Mono */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 700;
src:
url('../../../../resources/fonts/geist-mono-bold.woff2') format('woff2'),
url('../../../../resources/fonts/geist-mono-bold.woff') format('woff');
}
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 400;
src:
url('../../../../resources/fonts/geist-mono.woff2') format('woff2'),
url('../../../../resources/fonts/geist-mono.woff') format('woff');
}
:root {
--font-display: 'Leibniz Fraktur', serif;
--font-mono: 'Geist Mono', monospace;
}

View File

@@ -0,0 +1,192 @@
@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;
}
}
}
@mixin bonk-animation($delay: 0s, $duration: 1s, $angle: 25deg) {
--bonk-angle: #{$angle};
animation: bonk $duration $delay infinite step-end;
}

View File

@@ -0,0 +1,7 @@
// z-index
.lily-cursor {
z-index: 20;
}
.site-header {
z-index: 10;
}

View 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);
}

View 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;
}

View 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;
}

View File

@@ -0,0 +1,11 @@
@use 'sass:color';
:root {
@each $name, $theme in getThemes() {
.theme-#{$name} {
@each $name, $color in $theme {
--theme-#{$name}: #{$color};
}
}
}
}

View 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%);
}

View 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;
}
}

View 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));
}
}