Working theming, Layout->MainLayout, profiles list bugfix

This commit is contained in:
DarkCat09 2023-06-18 18:07:54 +04:00
parent 019fa83353
commit 2e272de59c
20 changed files with 681 additions and 169 deletions

11
public/less.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/less.min.js.map Normal file

File diff suppressed because one or more lines are too long

1
public/theme.less Symbolic link
View file

@ -0,0 +1 @@
../src/styles/theme.less

5
public/theme_dyn.less Normal file
View file

@ -0,0 +1,5 @@
@import url(/theme.less);
@accent: #6570d6;
@bdrs: 2rem;
@purebg: false;
body { .theme-mixin(@accent, @bdrs, @purebg) !important; }

View file

@ -0,0 +1,46 @@
---
export interface Props {
name: string;
hex: string;
}
const { name, hex } = Astro.props;
---
<a href="javascript:void(0)" class="color-button" data-hex={hex}>
<span>{name}</span>
</a>
<style lang="less" define:vars={{ hex }}>
.color-button {
padding: 0.15rem 0.3rem;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: var(--bdrs);
background: var(--hex);
color: #fff;
transition: filter 0.2s ease 0s;
&:hover {
color: #fff;
filter: brightness(80%);
}
}
</style>
<script>
const accentInput = document.getElementById('accent') as HTMLInputElement
const btns = document.querySelectorAll('.color-button')
for (let btn of btns) {
btn.addEventListener('click', ev => {
const elem = ev.currentTarget as HTMLAnchorElement
accentInput.value = elem.dataset.hex
accentInput.dispatchEvent(new Event('input'))
})
}
</script>

View file

@ -0,0 +1,33 @@
---
import Icon from './Icon.astro';
export interface Props {
icon: string;
}
const { icon } = Astro.props;
---
<li>
<span class="icon-wrapper">
<Icon code={icon} />
</span>
<span class="content">
<slot />
</span>
</li>
<style lang="less">
li {
display: flex;
flex-direction: row;
align-items: center;
}
.icon-wrapper {
margin-right: 0.375rem;
width: 1.6rem;
font-size: 1.5rem;
text-align: center;
}
</style>

View file

@ -0,0 +1,40 @@
---
import Icon from './Icon.astro';
export interface Props {
name: string;
link: string;
icon: string;
}
const { name, link, icon } = Astro.props;
---
<li>
<a href={link}>
<span class="icon-wrapper">
<Icon code={icon} />
</span>
{name}
</a>
</li>
<style lang="less">
li {
// 0.6rem = horizontal padding
padding: 0 0.6rem;
font-size: 1.15rem;
}
a {
display: flex;
flex-direction: row;
align-items: center;
}
.icon-wrapper {
margin-right: 0.25rem;
font-size: 1.5rem;
color: var(--fg-sec);
}
</style>

View file

@ -43,14 +43,18 @@ const { app, url, name, icon, copyName = false } = Astro.props;
</script>
<style lang="less">
@import "/src/styles/profile_calc.less";
.profile {
padding: 0.4rem 0.6rem;
// vertical horizontal
padding: 0.4rem @profile-horiz-padding;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 2rem;
width: @profile-width;
border-radius: var(--bdrs);
&:hover {
background: var(--accent-bg);
@ -58,8 +62,6 @@ const { app, url, name, icon, copyName = false } = Astro.props;
}
a.profile-main {
@icon-size: 2.3rem;
flex-grow: 1;
display: flex;
@ -70,13 +72,12 @@ const { app, url, name, icon, copyName = false } = Astro.props;
text-decoration: none;
.icon-wrapper {
font-size: @icon-size;
margin-right: 0.4rem;
font-size: @profile-icon-app;
margin-right: @profile-icon-margin;
}
}
a.copy {
@icon-size: 1.8rem;
position: relative;
font-size: 1.1rem;
@ -86,8 +87,8 @@ const { app, url, name, icon, copyName = false } = Astro.props;
align-items: center;
.icon-wrapper {
font-size: @icon-size;
margin-left: 0.4rem;
font-size: @profile-icon-copy;
margin-left: @profile-icon-margin;
}
}
@ -95,6 +96,9 @@ const { app, url, name, icon, copyName = false } = Astro.props;
display: flex;
flex-direction: column;
width: @profile-text-width;
overflow: hidden;
.app {
font-weight: 600;
}

View file

@ -0,0 +1,82 @@
---
import Input from "../layouts/Input.astro";
export interface Props {
id: string;
label: string;
min: number;
max: number;
value: number;
}
const { id, label, min, max, value } = Astro.props;
---
<Input label="" column>
<span slot="label">
{label}:
<span class="range-value">2</span>
rem
</span>
<span slot="after">
<input type="range" name={id} id={id} min={min} max={max} value={value}>
</span>
</Input>
<style lang="less">
input {
-webkit-appearance: none;
appearance: none;
border: none;
outline: 0;
background: var(--bg);
&:focus {
outline: 0;
}
@track-height: 0.4rem;
@thumb-size: 1.05rem;
@thumb-shadow-blur: @thumb-size * 0.5;
// line
&::-webkit-slider-runnable-track,
&::-moz-range-track {
height: @track-height;
background: var(--bg-sec);
border-radius: @track-height * 0.5;
}
// circle
&::-webkit-slider-thumb,
&::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
width: @thumb-size;
height: @thumb-size;
border-radius: @thumb-size;
border: none;
//background: var(--bg);
background: #fff;
// x-offset y-offset blur color
box-shadow: 0 0 @thumb-shadow-blur var(--fg);
}
}
</style>
<script>
const ranges = document.querySelectorAll('input[type="range"]')
const handler = (ev: Event) => {
const input = ev.currentTarget as HTMLInputElement
const container = input.parentElement.parentElement
const valueSpan = container.querySelector('.range-value') as HTMLSpanElement
valueSpan.innerText = input.value
}
for (let elem of ranges) {
elem.addEventListener('input', handler)
elem.addEventListener('updatedByJavascript', handler)
elem.dispatchEvent(new Event('updatedByJavascript'))
}
</script>

View file

@ -26,9 +26,14 @@ const { id, label, checked = false } = Astro.props;
@switch-width: 2.5rem;
@switch-height: @switch-width * 0.5;
@switch-padding: @switch-width * 0.08;
@switch-bdrs: @switch-width * 0.4;
@circle-size: @switch-width * 0.4;
@circle-margin: @switch-width * 0.04;
@circle-margin: @switch-width * 0.05;
.input-wrapper {
padding-top: @switch-padding;
}
.switch {
display: inline-flex;

View file

@ -1,22 +1,48 @@
---
import Input from '../layouts/Input.astro';
export interface Props {
id: string;
label: string;
value?: string;
placeholder?: string;
regex?: string | null;
regex?: string;
size?: number;
}
const { id, label, placeholder = "", regex = null } = Astro.props;
const { id, label, value = "", placeholder = "", regex = ".*", size = 15 } = Astro.props;
---
<label>
<span class="label">{label}</span>
<input type="text" name={id} id={id} placeholder={placeholder} pattern={regex}>
</label>
<Input label={label}>
<span slot="after">
<input type="text" name={id} id={id} value={value} placeholder={placeholder} pattern={regex} size={size}>
</span>
</Input>
<style lang="less">
input {
margin-left: 0.5rem;
padding: 0.2rem;
background: var(--sec-bg);
color: var(--fg);
font-size: 0.9rem;
outline: 0;
border: none;
border-bottom: 0.125rem solid var(--bg-sec);
transition: border-bottom-color 0.2s ease 0s;
&:focus {
border-bottom-color: var(--accent);
}
&:hover {
border-bottom-color: var(--accent-hl);
}
&::placeholder {
color: var(--fg-sec);
}
}
</style>

View file

@ -1,21 +1,42 @@
---
import Input from "../layouts/Input.astro";
import Modal from "../layouts/Modal.astro";
import ColorBtn from "./ColorBtn.astro";
import Slider from "./Slider.astro";
import Switch from "./Switch.astro";
import TextBox from "./TextBox.astro";
---
<Modal id="theme">
<noscript>
<noscript class="hint">
These options will <b>not</b> work without JavaScript
</noscript>
<Switch id="dark" label="Dark theme" checked />
<Switch id="custom-theme" label="Set custom colors" />
<fieldset class="first">
<Switch id="dark" label="Dark theme" checked />
<Switch id="custom-theme" label="Advanced (&beta;eta)" />
</fieldset>
<section id="custom-colors">
<TextBox id="accent" label="Accent color" placeholder="HEX (#fff)" regex="#?(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})" />
<label>
<span class="label">Accent color</span>
<input type="color" name="accent" id="accent">
<label class="hint">
You may need to reload the page
after enabling/disabling "Advanced"
</label>
<fieldset>
<Switch id="purebg" label="Pure background" />
</fieldset>
<fieldset>
<TextBox id="accent" label="Accent color" placeholder="HEX (#fff)" regex="#?(?:[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" size={7} />
<Input label="">
<span class="color-btns" slot="after">
<ColorBtn name="red" hex="#ba505a" />
<ColorBtn name="yellow" hex="#b9aa50" />
<ColorBtn name="green" hex="#5bab65" />
<ColorBtn name="blue" hex="#6570d6" />
</span>
</Input>
</fieldset>
<fieldset>
<Slider id="bdrs" label="Border radius" min={0} max={4} value={2} />
</fieldset>
</section>
</Modal>
@ -28,137 +49,189 @@ import TextBox from "./TextBox.astro";
flex-direction: column;
}
}
.color-btns {
margin-top: 0.2rem;
}
.hint {
width: 18rem;
font-size: 0.9rem;
color: var(--fg-sec);
}
</style>
<script>
const colorsSection = document.getElementById('custom-colors')
var lessLoaded = false
class ThemingOption {
private cookieKey: string
private cookieSwitch: Array<string | RegExp>
private callback: (input: HTMLInputElement, state: boolean) => any
private getval: (input: HTMLInputElement) => any
private media: string | null
private _input: HTMLInputElement | null
get input() { return this._input }
input: HTMLInputElement | undefined
onchange:
(opt: ThemingOption, upd: boolean) => any =
(_opt, _upd) => {}
constructor (
cookieKey: string,
cookieSwitch: Array<string | RegExp> | null,
callback: (input: HTMLInputElement, state: boolean) => any,
getval: (input: HTMLInputElement) => any,
input: HTMLInputElement | null = null,
media: string | null = null,
inputId: string,
onchange: (opt: ThemingOption, upd: boolean) => any
) {
this.cookieKey = cookieKey
this.cookieSwitch = cookieSwitch
this.callback = callback
this.getval = getval
this._input = input
this.media = media
}
this.input = document.getElementById(inputId) as HTMLInputElement
this.onchange = onchange
/*
function themeCheck() {
if (
document.cookie.includes('dc09_dark=1') || (
matchMedia('(prefers-color-scheme: dark)') &&
!document.cookie.includes('dc09_dark=0')
)
) {
document.body.dataset.dark = '1'
switchDark.checked = true
}
else {
document.body.dataset.dark = '0'
switchDark.checked = false
}
this.onchange(this, false)
if (!this.input) return
this.input.addEventListener(
'input',
() => this.onchange(this, true),
)
}
}
/** Converts string to boolean, mainly for localStorage.
* Returned `null` means that the key was unset.
*/
const toBool = (val: string | null) => {
switch (val) {
case undefined:
case null:
case '':
return null
case 'false':
case '0':
return false
default:
return true
}
}
const colorRegex = /#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/
/** Parses color-like string and
* returns the color in the general format.
* Returned `undefined` means that it's not a color.
* If `val` is `null`, returns `null`.
*/
const toColor = (val: string | null) => {
if (!val) return null
return (val.match(colorRegex) || [])[1]
}
new ThemingOption(
'dark',
(opt, upd) => {
if (upd) {
const state = opt.input.checked
localStorage.setItem('dc09_dark', state ? '1' : '0')
return opt.onchange(opt, false)
}
Here is the rewritten themeCheck:
*/
check() {
const match = this.cookieSwitch.map(
value => document.cookie.match(
`${this.cookieKey}=${value}`
const stored = toBool(localStorage.getItem('dc09_dark'))
const media = matchMedia('(prefers-color-scheme: dark)')
const state = (
stored || (
stored === null &&
media.matches
)
)
opt.input.checked = state
document.body.dataset.dark = state ? '1' : '0'
}
)
if (
match[1] || (
this.media && !match[0] &&
matchMedia(this.media).matches
)
) {
if (this.callback)
this.callback(this._input, true)
const purebgOpt = new ThemingOption(
'purebg', // pure background
(opt, upd) => {
if (upd) {
const state = opt.input.checked
localStorage.setItem('dc09_purebg', state ? '1' : '0')
reloadTheme()
return opt.onchange(opt, false)
}
opt.input.checked = toBool(localStorage.getItem('dc09_purebg'))
}
)
const accentOpt = new ThemingOption(
'accent',
(opt, upd) => {
if (upd) {
if (!opt.input.validity.valid) return
const color = toColor(opt.input.value)
if (!color) return
localStorage.setItem('dc09_accent', color)
reloadTheme()
return
}
if (this.callback)
this.callback(this._input, false)
const color = localStorage.getItem('dc09_accent') || '6570d6'
opt.input.value = color
}
)
update() {
const value = this.getval(this._input)
const localhost = window.location.hostname == 'localhost'
const domain = localhost ? 'localhost' : 'dc09.ru'
const cookie = `${this.cookieKey}=${value};domain=${domain};samesite=lax`
document.cookie = cookie
this.check()
const bdrsOpt = new ThemingOption(
'bdrs', // border-radius, as in Emmet
(opt, upd) => {
if (upd) {
const value = opt.input.value
localStorage.setItem('dc09_bdrs', value)
reloadTheme()
return
}
let value = Number(localStorage.getItem('dc09_bdrs'))
value = isNaN(value) ? 2 : value
opt.input.value = String(value)
opt.input.dispatchEvent(new Event('updatedByJavascript'))
}
)
const colorsSection = document.getElementById('custom-colors')
new ThemingOption(
'custom-theme',
(opt, upd) => {
if (upd) {
const state = opt.input.checked
localStorage.setItem('dc09_custom', state ? '1' : '0')
return opt.onchange(opt, false)
}
const state = toBool(localStorage.getItem('dc09_custom'))
opt.input.checked = state
colorsSection.dataset.show = state ? '1' : '0'
if (state)
reloadLess()
}
)
function reloadLess() {
if (!lessLoaded) {
// Load `theme_dyn.less`
const lessStyles = document.createElement('link')
lessStyles.rel = 'stylesheet/less'
lessStyles.type = 'text/css'
lessStyles.href = '/theme_dyn.less'
document.head.append(lessStyles)
// Create LessCSS script element
const lessScript = document.createElement('script')
lessScript.src = '/less.min.js'
// Modify variables when less is loaded
lessScript.addEventListener('load', reloadTheme)
// Load LessCSS script
document.head.append(lessScript)
}
else
reloadTheme()
}
const opts = {
dark: new ThemingOption(
'dc09_dark', ['0', '1'],
(input: HTMLInputElement, state: boolean) => {
input.checked = state
document.body.dataset.dark = state ? '1' : '0'
},
(input: HTMLInputElement) => input.checked ? 1 : 0,
document.getElementById('dark') as HTMLInputElement,
'(prefers-color-scheme: dark)',
),
custom: new ThemingOption(
'dc09_custom', ['0', '1'],
(input: HTMLInputElement, state: boolean) => {
input.checked = state
colorsSection.dataset.show = state ? '1' : '0'
if (state)
loadCustomTheme()
},
(input: HTMLInputElement) => input.checked ? 1 : 0,
document.getElementById('custom-theme') as HTMLInputElement,
)
}
for (let optObj of Object.values(opts)) {
((opt) => {
opt.check()
opt.input.addEventListener('input', () => opt.update())
})(optObj)
}
function loadCustomTheme() {
// Load `theme.less`
const lessStyles = document.createElement('link')
lessStyles.rel = 'stylesheet/less'
lessStyles.type = 'text/css'
lessStyles.href = '/src/styles/theme.less'
document.head.append(lessStyles)
// Load LessCSS script element
const lessScript = document.createElement('script')
lessScript.src = 'https://cdn.jsdelivr.net/npm/less'
// Replace variables
lessScript.onload = () => {
eval('less').modifyVars({
'accent': '#50aa70',
})
}
// Load LessCSS
document.head.append(lessScript)
function reloadTheme() {
const accent = '#' + toColor(accentOpt.input.value)
const bdrs = Number(bdrsOpt.input.value)
const purebg = purebgOpt.input.checked
eval('less').modifyVars({
accent: accent,
bdrs: `${bdrs}rem`,
purebg: purebg,
})
}
</script>

View file

@ -1,26 +1,47 @@
---
export interface Props {
label: string;
column?: boolean;
}
const { label } = Astro.props;
const { label, column = false } = Astro.props;
const flexDirection = column ? "column" : "row";
const flexAlign = column ? "start" : "center";
const cursor = column ? "normal" : "pointer";
---
<label>
<slot name="before" />
<span class="label">{label}</span>
<slot name="label">
<span class="label">
{label}
</span>
</slot>
<slot name="after" />
</label>
<style lang="less">
<style lang="less" define:vars={{ flexDirection, flexAlign, cursor }}>
label {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
flex-direction: var(--flexDirection);
align-items: var(--flexAlign);
cursor: var(--cursor);
&:not(:first-of-type) {
margin-top: 0.25rem;
margin-top: 0.5rem;
}
:global(fieldset) {
margin: 0;
padding: 0;
border: none;
&.first > label:first-of-type {
margin-top: 0;
}
& > label:not(:first-of-type) {
margin-top: 0;
}
}
</style>

View file

@ -1,11 +1,10 @@
---
import Menu from '../components/Menu.astro';
export interface Props {
title: string;
description: string;
}
const { title } = Astro.props;
const { title, description } = Astro.props;
---
<!DOCTYPE html>
@ -14,21 +13,22 @@ const { title } = Astro.props;
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<meta name="description" content="Hi! I'm Andrew aka DarkCat09, a 13-years-old fullstack developer from Russia" />
<meta name="description" content={description} />
<slot name="metadata" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<Menu />
<slot />
</body>
</html>
<style lang="less" is:global>
@import "/src/styles/theme.less";
body {
.theme-mixin();
margin: 0;
padding: 0;
@ -46,10 +46,22 @@ const { title } = Astro.props;
max-width: 300rem;
}
article {
max-width: 100vw;
width: fit-content;
margin: auto;
}
section {
margin: 0.5rem 0;
}
.card {
border: 0.125rem dashed var(--bg-sec);
border-radius: var(--bdrs);
padding: 0.625rem;
}
hgroup > * {
margin: 0;
@ -63,7 +75,7 @@ const { title } = Astro.props;
h1 { font-size: 2.4rem; }
a, .link {
color: var(--accent);
color: var(--accent-fg);
text-decoration: none;
&:hover {

View file

@ -0,0 +1,24 @@
---
import Layout from "./Layout.astro";
import Menu from "./Menu.astro";
import MenuItem from "../components/MenuItem.astro";
export interface Props {
page: string;
description: string;
}
const { page, description } = Astro.props;
---
<Layout title={"DarkCat09 | " + page} description={description}>
<Menu>
<MenuItem name="Homepage" icon="&#xf015;" link="/" />
<MenuItem name="Projects" icon="&#xf121;" link="/projects" />
<MenuItem name="Services" icon="&#xf233;" link="/services" />
<MenuItem name="Blog" icon="&#xf1ea;" link="/blog" />
</Menu>
<main>
<slot />
</main>
</Layout>

32
src/layouts/Menu.astro Normal file
View file

@ -0,0 +1,32 @@
---
import MenuItem from "../components/MenuItem.astro";
import ThemeModal from "../components/ThemeModal.astro";
---
<ThemeModal />
<nav>
<ul>
<slot />
</ul>
<ul>
<MenuItem name="Theme" icon="&#xf53f;" link="#theme" />
</ul>
</nav>
<style lang="less">
nav {
margin: 0.4rem;
display: flex;
flex-direction: row;
justify-content: space-between;
}
ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
</style>

View file

@ -56,7 +56,7 @@ const { id } = Astro.props;
padding: 1rem;
z-index: 102;
border-radius: 2rem;
border-radius: var(--bdrs);
background: var(--bg);
}

View file

@ -1,36 +1,103 @@
---
import Layout from '../layouts/Layout.astro';
import MainLayout from '../layouts/MainLayout.astro';
import Profile from '../components/Profile.astro';
import ListItem from '../components/ListItem.astro';
const description =
"Hi! I'm Andrew aka DarkCat09, " +
"a 13-years-old fullstack developer from Russia";
---
<Layout title="DarkCat09 | Homepage">
<main>
<MainLayout page="Homepage" description={description}>
<article class="centered">
<hgroup>
<h1>Hi! I'm Andrew.</h1>
<h2>13-years-old fullstack dev</h2>
<h2><span id="age">13</span>-years-old fullstack dev</h2>
</hgroup>
<section id="profiles">
<Profile app="Gitea" icon="&#xf1d3;" name="DarkCat09" url="https://git.dc09.ru/DarkCat09" />
<Profile app="GitHub" icon="&#xf09b;" name="DarkCat09" url="https://github.com/DarkCat09" />
<Profile app="E-mail" icon="&#xf0e0;" name="darkcat09@vivaldi.net" url="mailto:darkcat09@vivaldi.net" copyName />
<Profile app="Telegram" icon="&#xf2c6;" name="@darkcat09" url="https://t.me/darkcat09" />
<Profile app="Matrix" icon="&#xf4ad;" name="darkcat09@dc09.ru" url="https://matrix.to/#/@darkcat09:dc09.ru" copyName />
<Profile app="Matrix" icon="&#xf4ad;" name="darkcat09:dc09.ru" url="https://matrix.to/#/@darkcat09:dc09.ru" copyName />
<Profile app="Discord" icon="&#xf392;" name="DarkCat09#5587" url="https://discord.com/app" copyName />
</section>
</main>
</Layout>
</article>
<article class="card" id="about">
<ul>
<ListItem icon="&#xf05a;">
<a href="https://t.me/dc09about" target="_blank">
https://t.me/dc09about
</a>
</ListItem>
<ListItem icon="&#xf5a0;">
Russia, Ulyanovsk
</ListItem>
<ListItem icon="&#xf025;">
Favorite music bands:
</ListItem>
<ListItem icon="">
<ul>
<li>Linkin Park</li>
<li>One Direction</li>
<li>Set It Off</li>
<li>Дайте танк (!)</li>
<li>Научно-технический рэп</li>
<li>Imagine Dragons</li>
</ul>
</ListItem>
<ListItem icon="&#xf108;">
Favorite OS: Manjaro Linux
</ListItem>
<ListItem icon="">
<code>
<b>CPU:</b> 6-core Intel Core i5-10400 (-MT MCP-) <b>speed/min/max:</b> 2725/800/4300 MHz<br />
<b>Kernel:</b> 6.3.4-1-MANJARO x86_64 <b>Up:</b> 3h 32m <b>Mem:</b> 6763.3/30935.8 MiB (21.9%)<br />
<b>Storage:</b> 1.36 TiB (15.8% used) <b>Procs:</b> 303 <b>Shell:</b> Zsh <b>inxi:</b> 3.3.27
</code>
</ListItem>
</ul>
</article>
</MainLayout>
<style lang="less">
@import "/src/styles/profile_calc.less";
#profiles {
display: grid;
grid-template-columns: repeat(1, auto);
@media (min-width: 450px) {
@media (min-width: @profile-width * 2) {
grid-template-columns: repeat(2, auto);
}
@media (min-width: 670px) {
@media (min-width: @profile-width * 3) {
grid-template-columns: repeat(3, auto);
}
}
#about > ul {
list-style: none;
margin: 0;
padding: 0;
}
code {
font-family: 'Hack', 'Liberation Mono', 'Source Code Pro', monospace;
font-size: 0.9rem;
}
</style>
<script>
// in milliseconds
const ageMs = Date.now() - 1247481000000
// in years
let age = ageMs / (1000 * 60 * 60 * 24 * 365.25)
// fallback to default value
// + rounding
age = (age < 13 ? 13 : Math.floor(age))
document.getElementById('age').innerText = String(age)
</script>

View file

@ -0,0 +1,9 @@
@profile-icon-app: 2.3rem;
@profile-icon-copy: 1.8rem;
@profile-text-width: 9.5rem;
@profile-icon-margin: 0.4rem;
@profile-horiz-padding: 0.6rem;
@profile-width: @profile-horiz-padding + @profile-icon-app + @profile-icon-margin + @profile-text-width + @profile-icon-margin + @profile-icon-copy + @profile-horiz-padding;

View file

@ -1,30 +1,50 @@
@accent: #5070ca;
.theme-mixin(@accent: #6570d6, @bdrs: 2rem, @purebg: false) {
body {
@bg-light: #fff;
@fg-light: #202020;
@bg-pure-light: #fff;
@bg-colored-light: hsl(
hue(@accent),
15%, 95%,
);
@bg-dark: #222229;
@fg-dark: #eee;
@bg-light: if(@purebg, @bg-pure-light, @bg-colored-light); // background
@fg-light: #202020; // foreground
@bg-pure-dark: #222;
@bg-colored-dark: hsl(
hue(@accent),
5%, 15%,
);
@bg-dark: if(@purebg, @bg-pure-dark, @bg-colored-dark); // background
@fg-dark: #eee; // foreground
--accent: @accent;
--bdrs: @bdrs;
.light-mixin() {
--bg: @bg-light;
--fg: @fg-light;
--bg-sec: darken(@bg-light, 30%);
--fg-sec: lighten(@fg-light, 15%);
--accent-bg: darken(@bg-light, 10%);
--accent-hl: darken(@accent, 15%);
--fg-sec: lighten(@fg-light, 25%);
@accent-fg: darken(@accent, 7%);
--accent-bg: darken(@bg-light, 8%);
--accent-fg: @accent-fg;
--accent-hl: darken(@accent-fg, 15%);
}
.dark-mixin() {
--bg: @bg-dark;
--fg: @fg-dark;
--bg-sec: lighten(@bg-dark, 15%);
--fg-sec: darken(@bg-light, 25%);
--fg-sec: darken(@fg-dark, 30%);
@accent-fg: lighten(@accent, 7%);
--accent-bg: lighten(@bg-dark, 5%);
--accent-hl: lighten(@accent, 15%);
--accent-fg: @accent-fg;
--accent-hl: lighten(@accent-fg, 15%);
}
.dark-mixin();