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

View file

@ -1,22 +1,48 @@
--- ---
import Input from '../layouts/Input.astro';
export interface Props { export interface Props {
id: string; id: string;
label: string; label: string;
value?: string;
placeholder?: 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> <Input label={label}>
<span class="label">{label}</span> <span slot="after">
<input type="text" name={id} id={id} placeholder={placeholder} pattern={regex}> <input type="text" name={id} id={id} value={value} placeholder={placeholder} pattern={regex} size={size}>
</label> </span>
</Input>
<style lang="less"> <style lang="less">
input { input {
margin-left: 0.5rem;
padding: 0.2rem;
background: var(--sec-bg); background: var(--sec-bg);
color: var(--fg); 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> </style>

View file

@ -1,21 +1,42 @@
--- ---
import Input from "../layouts/Input.astro";
import Modal from "../layouts/Modal.astro"; import Modal from "../layouts/Modal.astro";
import ColorBtn from "./ColorBtn.astro";
import Slider from "./Slider.astro";
import Switch from "./Switch.astro"; import Switch from "./Switch.astro";
import TextBox from "./TextBox.astro"; import TextBox from "./TextBox.astro";
--- ---
<Modal id="theme"> <Modal id="theme">
<noscript> <noscript class="hint">
These options will <b>not</b> work without JavaScript These options will <b>not</b> work without JavaScript
</noscript> </noscript>
<Switch id="dark" label="Dark theme" checked /> <fieldset class="first">
<Switch id="custom-theme" label="Set custom colors" /> <Switch id="dark" label="Dark theme" checked />
<Switch id="custom-theme" label="Advanced (&beta;eta)" />
</fieldset>
<section id="custom-colors"> <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 class="hint">
<label> You may need to reload the page
<span class="label">Accent color</span> after enabling/disabling "Advanced"
<input type="color" name="accent" id="accent">
</label> </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> </section>
</Modal> </Modal>
@ -28,137 +49,189 @@ import TextBox from "./TextBox.astro";
flex-direction: column; flex-direction: column;
} }
} }
.color-btns {
margin-top: 0.2rem;
}
.hint {
width: 18rem;
font-size: 0.9rem;
color: var(--fg-sec);
}
</style> </style>
<script> <script>
const colorsSection = document.getElementById('custom-colors') var lessLoaded = false
class ThemingOption { class ThemingOption {
private cookieKey: string input: HTMLInputElement | undefined
private cookieSwitch: Array<string | RegExp> onchange:
private callback: (input: HTMLInputElement, state: boolean) => any (opt: ThemingOption, upd: boolean) => any =
private getval: (input: HTMLInputElement) => any (_opt, _upd) => {}
private media: string | null
private _input: HTMLInputElement | null
get input() { return this._input }
constructor ( constructor (
cookieKey: string, inputId: string,
cookieSwitch: Array<string | RegExp> | null, onchange: (opt: ThemingOption, upd: boolean) => any
callback: (input: HTMLInputElement, state: boolean) => any,
getval: (input: HTMLInputElement) => any,
input: HTMLInputElement | null = null,
media: string | null = null,
) { ) {
this.cookieKey = cookieKey this.input = document.getElementById(inputId) as HTMLInputElement
this.cookieSwitch = cookieSwitch this.onchange = onchange
this.callback = callback
this.getval = getval
this._input = input
this.media = media
}
/* this.onchange(this, false)
function themeCheck() { if (!this.input) return
if ( this.input.addEventListener(
document.cookie.includes('dc09_dark=1') || ( 'input',
matchMedia('(prefers-color-scheme: dark)') && () => this.onchange(this, true),
!document.cookie.includes('dc09_dark=0') )
) }
) { }
document.body.dataset.dark = '1'
switchDark.checked = true /** Converts string to boolean, mainly for localStorage.
} * Returned `null` means that the key was unset.
else { */
document.body.dataset.dark = '0' const toBool = (val: string | null) => {
switchDark.checked = false 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: const stored = toBool(localStorage.getItem('dc09_dark'))
*/ const media = matchMedia('(prefers-color-scheme: dark)')
check() {
const match = this.cookieSwitch.map( const state = (
value => document.cookie.match( stored || (
`${this.cookieKey}=${value}` stored === null &&
media.matches
) )
) )
opt.input.checked = state
document.body.dataset.dark = state ? '1' : '0'
}
)
if ( const purebgOpt = new ThemingOption(
match[1] || ( 'purebg', // pure background
this.media && !match[0] && (opt, upd) => {
matchMedia(this.media).matches if (upd) {
) const state = opt.input.checked
) { localStorage.setItem('dc09_purebg', state ? '1' : '0')
if (this.callback) reloadTheme()
this.callback(this._input, true) 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 return
} }
if (this.callback) const color = localStorage.getItem('dc09_accent') || '6570d6'
this.callback(this._input, false) opt.input.value = color
} }
)
update() { const bdrsOpt = new ThemingOption(
const value = this.getval(this._input) 'bdrs', // border-radius, as in Emmet
const localhost = window.location.hostname == 'localhost' (opt, upd) => {
const domain = localhost ? 'localhost' : 'dc09.ru' if (upd) {
const cookie = `${this.cookieKey}=${value};domain=${domain};samesite=lax` const value = opt.input.value
document.cookie = cookie localStorage.setItem('dc09_bdrs', value)
this.check() 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 = { function reloadTheme() {
dark: new ThemingOption( const accent = '#' + toColor(accentOpt.input.value)
'dc09_dark', ['0', '1'], const bdrs = Number(bdrsOpt.input.value)
(input: HTMLInputElement, state: boolean) => { const purebg = purebgOpt.input.checked
input.checked = state eval('less').modifyVars({
document.body.dataset.dark = state ? '1' : '0' accent: accent,
}, bdrs: `${bdrs}rem`,
(input: HTMLInputElement) => input.checked ? 1 : 0, purebg: purebg,
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)
} }
</script> </script>

View file

@ -1,26 +1,47 @@
--- ---
export interface Props { export interface Props {
label: string; 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> <label>
<slot name="before" /> <slot name="before" />
<span class="label">{label}</span> <slot name="label">
<span class="label">
{label}
</span>
</slot>
<slot name="after" /> <slot name="after" />
</label> </label>
<style lang="less"> <style lang="less" define:vars={{ flexDirection, flexAlign, cursor }}>
label { label {
display: flex; display: flex;
flex-direction: row; flex-direction: var(--flexDirection);
align-items: center; align-items: var(--flexAlign);
cursor: pointer; cursor: var(--cursor);
&:not(:first-of-type) { margin-top: 0.5rem;
margin-top: 0.25rem; }
: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> </style>

View file

@ -1,11 +1,10 @@
--- ---
import Menu from '../components/Menu.astro';
export interface Props { export interface Props {
title: string; title: string;
description: string;
} }
const { title } = Astro.props; const { title, description } = Astro.props;
--- ---
<!DOCTYPE html> <!DOCTYPE html>
@ -14,21 +13,22 @@ const { title } = Astro.props;
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} /> <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" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title> <title>{title}</title>
</head> </head>
<body> <body>
<Menu />
<slot /> <slot />
</body> </body>
</html> </html>
<style lang="less" is:global> <style lang="less" is:global>
@import "/src/styles/theme.less"; @import "/src/styles/theme.less";
body { body {
.theme-mixin();
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -46,10 +46,22 @@ const { title } = Astro.props;
max-width: 300rem; max-width: 300rem;
} }
article {
max-width: 100vw;
width: fit-content;
margin: auto;
}
section { section {
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.card {
border: 0.125rem dashed var(--bg-sec);
border-radius: var(--bdrs);
padding: 0.625rem;
}
hgroup > * { hgroup > * {
margin: 0; margin: 0;
@ -63,7 +75,7 @@ const { title } = Astro.props;
h1 { font-size: 2.4rem; } h1 { font-size: 2.4rem; }
a, .link { a, .link {
color: var(--accent); color: var(--accent-fg);
text-decoration: none; text-decoration: none;
&:hover { &: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; padding: 1rem;
z-index: 102; z-index: 102;
border-radius: 2rem; border-radius: var(--bdrs);
background: var(--bg); 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 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"> <MainLayout page="Homepage" description={description}>
<main> <article class="centered">
<hgroup> <hgroup>
<h1>Hi! I'm Andrew.</h1> <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> </hgroup>
<section id="profiles"> <section id="profiles">
<Profile app="Gitea" icon="&#xf1d3;" name="DarkCat09" url="https://git.dc09.ru/DarkCat09" /> <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="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="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="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 /> <Profile app="Discord" icon="&#xf392;" name="DarkCat09#5587" url="https://discord.com/app" copyName />
</section> </section>
</main> </article>
</Layout> <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"> <style lang="less">
@import "/src/styles/profile_calc.less";
#profiles { #profiles {
display: grid; display: grid;
grid-template-columns: repeat(1, auto); grid-template-columns: repeat(1, auto);
@media (min-width: 450px) { @media (min-width: @profile-width * 2) {
grid-template-columns: repeat(2, auto); grid-template-columns: repeat(2, auto);
} }
@media (min-width: 670px) { @media (min-width: @profile-width * 3) {
grid-template-columns: repeat(3, auto); 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> </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-pure-light: #fff;
@bg-light: #fff; @bg-colored-light: hsl(
@fg-light: #202020; hue(@accent),
15%, 95%,
);
@bg-dark: #222229; @bg-light: if(@purebg, @bg-pure-light, @bg-colored-light); // background
@fg-dark: #eee; @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; --accent: @accent;
--bdrs: @bdrs;
.light-mixin() { .light-mixin() {
--bg: @bg-light; --bg: @bg-light;
--fg: @fg-light; --fg: @fg-light;
--bg-sec: darken(@bg-light, 30%); --bg-sec: darken(@bg-light, 30%);
--fg-sec: lighten(@fg-light, 15%); --fg-sec: lighten(@fg-light, 25%);
--accent-bg: darken(@bg-light, 10%);
--accent-hl: darken(@accent, 15%); @accent-fg: darken(@accent, 7%);
--accent-bg: darken(@bg-light, 8%);
--accent-fg: @accent-fg;
--accent-hl: darken(@accent-fg, 15%);
} }
.dark-mixin() { .dark-mixin() {
--bg: @bg-dark; --bg: @bg-dark;
--fg: @fg-dark; --fg: @fg-dark;
--bg-sec: lighten(@bg-dark, 15%); --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-bg: lighten(@bg-dark, 5%);
--accent-hl: lighten(@accent, 15%); --accent-fg: @accent-fg;
--accent-hl: lighten(@accent-fg, 15%);
} }
.dark-mixin(); .dark-mixin();