Working theming, Layout->MainLayout, profiles list bugfix
This commit is contained in:
parent
019fa83353
commit
2e272de59c
20 changed files with 681 additions and 169 deletions
11
public/less.min.js
vendored
Normal file
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
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
1
public/theme.less
Symbolic link
|
@ -0,0 +1 @@
|
|||
../src/styles/theme.less
|
5
public/theme_dyn.less
Normal file
5
public/theme_dyn.less
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import url(/theme.less);
|
||||
@accent: #6570d6;
|
||||
@bdrs: 2rem;
|
||||
@purebg: false;
|
||||
body { .theme-mixin(@accent, @bdrs, @purebg) !important; }
|
46
src/components/ColorBtn.astro
Normal file
46
src/components/ColorBtn.astro
Normal 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>
|
33
src/components/ListItem.astro
Normal file
33
src/components/ListItem.astro
Normal 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>
|
40
src/components/MenuItem.astro
Normal file
40
src/components/MenuItem.astro
Normal 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>
|
|
@ -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;
|
||||
}
|
||||
|
|
82
src/components/Slider.astro
Normal file
82
src/components/Slider.astro
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (β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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
24
src/layouts/MainLayout.astro
Normal file
24
src/layouts/MainLayout.astro
Normal 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="" link="/" />
|
||||
<MenuItem name="Projects" icon="" link="/projects" />
|
||||
<MenuItem name="Services" icon="" link="/services" />
|
||||
<MenuItem name="Blog" icon="" link="/blog" />
|
||||
</Menu>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</Layout>
|
32
src/layouts/Menu.astro
Normal file
32
src/layouts/Menu.astro
Normal 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="" 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>
|
|
@ -56,7 +56,7 @@ const { id } = Astro.props;
|
|||
padding: 1rem;
|
||||
z-index: 102;
|
||||
|
||||
border-radius: 2rem;
|
||||
border-radius: var(--bdrs);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
|
|
|
@ -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="" name="DarkCat09" url="https://git.dc09.ru/DarkCat09" />
|
||||
<Profile app="GitHub" icon="" name="DarkCat09" url="https://github.com/DarkCat09" />
|
||||
<Profile app="E-mail" icon="" name="darkcat09@vivaldi.net" url="mailto:darkcat09@vivaldi.net" copyName />
|
||||
<Profile app="Telegram" icon="" name="@darkcat09" url="https://t.me/darkcat09" />
|
||||
<Profile app="Matrix" icon="" name="darkcat09@dc09.ru" url="https://matrix.to/#/@darkcat09:dc09.ru" copyName />
|
||||
<Profile app="Matrix" icon="" name="darkcat09:dc09.ru" url="https://matrix.to/#/@darkcat09:dc09.ru" copyName />
|
||||
<Profile app="Discord" icon="" name="DarkCat09#5587" url="https://discord.com/app" copyName />
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
</article>
|
||||
<article class="card" id="about">
|
||||
<ul>
|
||||
<ListItem icon="">
|
||||
<a href="https://t.me/dc09about" target="_blank">
|
||||
https://t.me/dc09about
|
||||
</a>
|
||||
</ListItem>
|
||||
<ListItem icon="">
|
||||
Russia, Ulyanovsk
|
||||
</ListItem>
|
||||
<ListItem icon="">
|
||||
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="">
|
||||
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>
|
||||
|
|
9
src/styles/profile_calc.less
Normal file
9
src/styles/profile_calc.less
Normal 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;
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue