initial-commit
This commit is contained in:
74
src/main/index.js
Normal file
74
src/main/index.js
Normal 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
20
src/preload/index.js
Normal 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
17
src/renderer/index.html
Normal 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
65
src/renderer/src/App.vue
Normal 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>
|
||||
69
src/renderer/src/components/Lenis.vue
Normal file
69
src/renderer/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>
|
||||
59
src/renderer/src/composables/useFloat.js
Normal file
59
src/renderer/src/composables/useFloat.js
Normal 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 }
|
||||
}
|
||||
14
src/renderer/src/composables/useLenis.js
Normal file
14
src/renderer/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/renderer/src/composables/useRelativeSize.js
Normal file
29
src/renderer/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/renderer/src/composables/useScrollProgress.js
Normal file
46
src/renderer/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/renderer/src/composables/useSmoothMouse.js
Normal file
89
src/renderer/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/renderer/src/composables/useSubmitForm.js
Normal file
34
src/renderer/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/renderer/src/composables/useTimelineTransition.js
Normal file
37
src/renderer/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/renderer/src/libs/math.js
Normal file
22
src/renderer/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/renderer/src/libs/sass-utils/index.js
Normal file
220
src/renderer/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/renderer/src/libs/sass-utils/utils.js
Normal file
108
src/renderer/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/renderer/src/libs/theme.js
Normal file
43
src/renderer/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,
|
||||
}
|
||||
5
src/renderer/src/main.js
Normal file
5
src/renderer/src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import './styles/main.scss'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
7
src/renderer/src/styles/_colors.scss
Normal file
7
src/renderer/src/styles/_colors.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
@use 'sass:color';
|
||||
|
||||
:root {
|
||||
@each $name, $color in getColors() {
|
||||
--#{$name}: #{$color};
|
||||
}
|
||||
}
|
||||
22
src/renderer/src/styles/_easings.scss
Normal file
22
src/renderer/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/renderer/src/styles/_font-style.scss
Normal file
31
src/renderer/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-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);
|
||||
}
|
||||
45
src/renderer/src/styles/_fonts.scss
Normal file
45
src/renderer/src/styles/_fonts.scss
Normal 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;
|
||||
}
|
||||
192
src/renderer/src/styles/_functions.scss
Normal file
192
src/renderer/src/styles/_functions.scss
Normal 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;
|
||||
}
|
||||
7
src/renderer/src/styles/_layers.scss
Normal file
7
src/renderer/src/styles/_layers.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
// z-index
|
||||
.lily-cursor {
|
||||
z-index: 20;
|
||||
}
|
||||
.site-header {
|
||||
z-index: 10;
|
||||
}
|
||||
91
src/renderer/src/styles/_layout.scss
Normal file
91
src/renderer/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/renderer/src/styles/_reset.scss
Normal file
99
src/renderer/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/renderer/src/styles/_scroll.scss
Normal file
34
src/renderer/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/renderer/src/styles/_themes.scss
Normal file
11
src/renderer/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/renderer/src/styles/_transitions.scss
Normal file
43
src/renderer/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/renderer/src/styles/_utils.scss
Normal file
42
src/renderer/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/renderer/src/styles/main.scss
Normal file
111
src/renderer/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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user