Minimalist
Got rid of (almost) everything. Bloat sinks the goat.
|
@ -1,179 +0,0 @@
|
||||||
/* Victor Westerlund */
|
|
||||||
|
|
||||||
/* -- Transition overrides -- */
|
|
||||||
|
|
||||||
body main .screen {
|
|
||||||
transition: var(--transition) transform, var(--transition) filter;
|
|
||||||
transition-delay: calc(var(--transition) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.active + .modal:nth-child(n+2),
|
|
||||||
body .modal.active ~ main .screen {
|
|
||||||
transition: var(--transition);
|
|
||||||
transition-delay: 1ms;
|
|
||||||
transform: scale(.95);
|
|
||||||
pointer-events: none;
|
|
||||||
filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.active + .modal {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal:first-child {
|
|
||||||
z-index: 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.active + .modal:nth-child(n+2) {
|
|
||||||
filter: blur(2px) brightness(.5);
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Boilerplate -- */
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
transition: var(--transition) transform, var(--transition) filter;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
pointer-events: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.active {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .button {
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .inner {
|
|
||||||
transition: var(--transition) transform, var(--transition) opacity;
|
|
||||||
position: relative;
|
|
||||||
background-color: var(--swatch-background);
|
|
||||||
width: calc(100vw - var(--padding));
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
word-break: break-word;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--padding);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: 0 3px 30px 0 rgba(33,33,33,.2);
|
|
||||||
opacity: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.active .inner {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .inner > h1,
|
|
||||||
.modal .inner > h2,
|
|
||||||
.modal .inner > p {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- */
|
|
||||||
|
|
||||||
.spinner.logo {
|
|
||||||
--size: clamp(30px,5vw,60px);
|
|
||||||
--anim-speed: 1s;
|
|
||||||
align-self: center;
|
|
||||||
margin-top: var(--padding);
|
|
||||||
margin-left: calc((var(--size) / 2) * -1);
|
|
||||||
animation: logoSpinner var(--anim-speed) infinite alternate linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error:first-line {
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logoSpinner {
|
|
||||||
to {
|
|
||||||
opacity: .1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- */
|
|
||||||
|
|
||||||
.modal h1 {
|
|
||||||
font-size: clamp(20px,2vw,20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal pre {
|
|
||||||
align-self: stretch;
|
|
||||||
overflow: scroll;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Cards -- */
|
|
||||||
|
|
||||||
.modal.card .inner {
|
|
||||||
align-self: flex-end;
|
|
||||||
transform: scale(.99) translateY(1vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.card.active .inner {
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.card .button[data-action="close"] {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Dialogs -- */
|
|
||||||
|
|
||||||
.modal.dialog .inner {
|
|
||||||
transform: scale(.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.dialog.active .inner {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-aspect-ratio: 14/9) {
|
|
||||||
/* -- Transition overrides -- */
|
|
||||||
|
|
||||||
body .modal {
|
|
||||||
transition-delay: calc(var(--transition) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
body .modal.active {
|
|
||||||
transition-delay: 1ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Boilerplate -- */
|
|
||||||
|
|
||||||
.modal.card .inner {
|
|
||||||
align-self: unset;
|
|
||||||
transform: scale(.99) translateY(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.dialog .inner {
|
|
||||||
width: unset;
|
|
||||||
min-width: 100px;
|
|
||||||
max-width: 50vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.dialog .button {
|
|
||||||
align-self: unset;
|
|
||||||
width: clamp(100px,100%,500px);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +1,10 @@
|
||||||
/* Victor Westerlund */
|
/* Victor Westerlund */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Component colors */
|
|
||||||
--palette-background: 255,255,255;
|
--palette-background: 255,255,255;
|
||||||
--palette-inverted: 0,0,0;
|
|
||||||
--palette-contrast: 33,33,33;
|
--palette-contrast: 33,33,33;
|
||||||
--palette-accent: 22,183,255;
|
|
||||||
|
|
||||||
/* Compiled colors */
|
|
||||||
--swatch-background: rgb(var(--palette-background));
|
|
||||||
--swatch-inverted: rgb(var(--palette-inverted));
|
|
||||||
--swatch-contrast: rgb(var(--palette-contrast));
|
|
||||||
--swatch-accent: rgb(var(--palette-accent));
|
|
||||||
|
|
||||||
/* Default styles */
|
|
||||||
--padding: 20px;
|
|
||||||
--border-radius: 10px;
|
|
||||||
--header-height: 100px;
|
|
||||||
--transition: 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--palette-background: 33,33,33;
|
|
||||||
--palette-inverted: 255,255,255;
|
|
||||||
--palette-contrast: 255,255,255;
|
|
||||||
--palette-accent: 255,255,255;
|
|
||||||
|
|
||||||
--swatch-background: rgb(var(--palette-background));
|
--swatch-background: rgb(var(--palette-background));
|
||||||
--swatch-inverted: rgb(var(--palette-inverted));
|
|
||||||
--swatch-contrast: rgb(var(--palette-contrast));
|
--swatch-contrast: rgb(var(--palette-contrast));
|
||||||
--swatch-accent: rgb(var(--palette-accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide {
|
|
||||||
display: none; /* Hide wide-screen elements */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
@ -67,430 +38,40 @@
|
||||||
color: var(--swatch-accent);
|
color: var(--swatch-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
|
||||||
picture {
|
|
||||||
text-decoration: none;
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--swatch-background);
|
background-color: var(--swatch-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
a {
|
||||||
width: 200vw;
|
display: content;
|
||||||
height: 100%;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.menuActive {
|
|
||||||
background-color: var(--swatch-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark.menuActive {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.menuActive main {
|
|
||||||
transform: translateX(-100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- */
|
|
||||||
|
|
||||||
.screen {
|
|
||||||
--background-pattern:
|
|
||||||
linear-gradient(90deg, var(--swatch-background) calc(var(--padding) + 1px), transparent 1%) center,
|
|
||||||
linear-gradient(var(--swatch-background) calc(var(--padding) + 1px), transparent 1%) center, var(--swatch-contrast);
|
|
||||||
--background-pattern-size: calc(var(--padding) + 2px) calc(var(--padding) + 2px);
|
|
||||||
|
|
||||||
width: 100vw;
|
|
||||||
background-color: var(--swatch-background);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .screen.dark {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen .inner {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen .content {
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: calc(var(--padding) * 1.5);
|
|
||||||
padding-top: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Positioning -- */
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- */
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
--size: 1em;
|
|
||||||
--skew: calc(var(--size) / 1.7);
|
|
||||||
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border: var(--skew) solid transparent;
|
|
||||||
border-top: var(--size) solid var(--swatch-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo::after {
|
|
||||||
content: "";
|
|
||||||
border: var(--skew) solid transparent;
|
|
||||||
border-top: var(--size) solid rgba(var(--palette-accent),.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- */
|
|
||||||
|
|
||||||
.button {
|
|
||||||
text-align: center;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-size: clamp(16px,5vw,22px);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
a.button,
|
|
||||||
p.button {
|
|
||||||
padding: unset;
|
|
||||||
display: inline;
|
|
||||||
font-size: inherit;
|
|
||||||
color: var(--swatch-accent);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.solid {
|
|
||||||
background-color: var(--swatch-contrast);
|
|
||||||
color: var(--swatch-background);
|
|
||||||
fill: var(--swatch-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.phantom {
|
|
||||||
background-color: rgba(var(--palette-inverted),.05);
|
|
||||||
color: var(--swatch-contrast);
|
|
||||||
fill: var(--swatch-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button svg {
|
|
||||||
pointer-events: none;
|
|
||||||
fill: inherit;
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button p {
|
|
||||||
pointer-events: none;
|
|
||||||
font-size: inherit;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.loading p {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.loading::after {
|
|
||||||
position: absolute;
|
|
||||||
content: "loading...";
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Screens -- */
|
|
||||||
|
|
||||||
header {
|
|
||||||
--size: var(--header-height,100px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--padding);
|
|
||||||
height: var(--size);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
header > *:nth-child(2) {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > *:nth-child(n+3) {
|
|
||||||
margin-left: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
header .hamburger {
|
|
||||||
width: calc(var(--size) - (var(--padding) * 2));
|
|
||||||
height: calc(var(--size) - (var(--padding) * 2));
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .hamburger div {
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--swatch-contrast);
|
|
||||||
box-shadow: 0 -10px 0 0 var(--swatch-contrast), 0 10px 0 0 var(--swatch-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
header .hamburger svg {
|
|
||||||
fill: none;
|
|
||||||
stroke: var(--swatch-contrast);
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .spacer {
|
|
||||||
width: 1px;
|
|
||||||
height: 80%;
|
|
||||||
background-color: rgba(var(--palette-contrast),.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark header .spacer {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .dark header .spacer {
|
|
||||||
background-color: rgba(var(--palette-contrast),.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
header .logo {
|
|
||||||
--size: 25px;
|
|
||||||
margin-top: calc(var(--size) / 2);
|
|
||||||
margin-right: calc(var(--size) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Screen > Landingpage -- */
|
|
||||||
|
|
||||||
.screen.landingpage {
|
|
||||||
background: var(--background-pattern);
|
|
||||||
background-size: var(--background-pattern-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.landingpage .content {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.landingpage img {
|
|
||||||
position: relative;
|
|
||||||
width: clamp(100px,80vw,40vh);
|
|
||||||
align-self: flex-end;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.landingpage .pattern {
|
|
||||||
position: absolute;
|
|
||||||
top: var(--header-height);
|
|
||||||
width: 100vw;
|
|
||||||
height: calc(100% - var(--header-height));
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.landingpage .pattern div {
|
|
||||||
--size: clamp(100px,100vw,35vh);
|
|
||||||
position: relative;
|
|
||||||
top: calc((var(--size) - var(--header-height)) * -1);
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border: solid var(--size) transparent;
|
|
||||||
border-bottom: solid calc(var(--size) * 2) rgba(var(--palette-accent),.1);
|
|
||||||
transform-origin: 50% 75%;
|
|
||||||
transform: rotate(20deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Screen > Menu -- */
|
|
||||||
|
|
||||||
.screen.menu .content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .button {
|
|
||||||
width: calc(100% - (var(--padding) * 2));
|
|
||||||
max-width: 400px;
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .button[data-value="contact"] {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* WIP */
|
|
||||||
.screen.menu .content > .narrow {
|
|
||||||
align-items: flex-start;
|
|
||||||
max-width: 900px;
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Media queries -- */
|
|
||||||
|
|
||||||
@media (max-width: 570px) {
|
|
||||||
.screen.menu .content > .narrow {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wide-screen */
|
|
||||||
@media (min-aspect-ratio: 14/9) and (min-height: 300px) {
|
|
||||||
/* -- Cornerstones -- */
|
|
||||||
|
|
||||||
.narrow,
|
|
||||||
header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
main {
|
||||||
width: 100vw;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
font-size: clamp(18px,5vw,3vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- Cornerstones > State overrides -- */
|
/* -- Content -- */
|
||||||
|
|
||||||
body.menuActive {
|
/* -- Media Queries -- */
|
||||||
background-color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.menuActive main {
|
@media (prefers-color-scheme: dark) {
|
||||||
transform: unset;
|
:root {
|
||||||
}
|
--palette-background: 0,0,0;
|
||||||
|
--palette-contrast: 255,255,255;
|
||||||
|
|
||||||
/* -- Screens -- */
|
|
||||||
|
|
||||||
.screen.menu,
|
|
||||||
.screen.landingpage {
|
|
||||||
width: 50vw;
|
|
||||||
flex-direction: row;
|
|
||||||
background: var(--background-pattern);
|
|
||||||
background-size: var(--background-pattern-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .screen {
|
|
||||||
--swatch-background: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Screens > Menu -- */
|
|
||||||
|
|
||||||
body:not(.dark) .screen.menu {
|
|
||||||
/* Component colors */
|
|
||||||
--palette-background: 255,255,255;
|
|
||||||
--palette-inverted: 0,0,0;
|
|
||||||
--palette-contrast: 33,33,33;
|
|
||||||
--palette-accent: 33,33,33;
|
|
||||||
|
|
||||||
/* Compiled colors */
|
|
||||||
--swatch-background: rgb(var(--palette-background));
|
--swatch-background: rgb(var(--palette-background));
|
||||||
--swatch-inverted: rgb(var(--palette-inverted));
|
|
||||||
--swatch-contrast: rgb(var(--palette-contrast));
|
--swatch-contrast: rgb(var(--palette-contrast));
|
||||||
--swatch-accent: rgb(var(--palette-accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .content {
|
|
||||||
padding-top: calc(var(--padding) * 1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: clamp(var(--padding),5vw,5vh);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide .group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide .logo {
|
|
||||||
--size: clamp(20px,3.5vw,5vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: clamp(20px,3vw,5vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide h1 span {
|
|
||||||
background: var(--swatch-contrast);
|
|
||||||
color: var(--swatch-background);
|
|
||||||
padding: 0 var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: clamp(16px,5vw,2vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide nav {
|
|
||||||
display: flex;
|
|
||||||
margin-top: calc(var(--padding) * 2);
|
|
||||||
gap: clamp(var(--padding),3vw,500px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .wide nav p {
|
|
||||||
display: inline-block;
|
|
||||||
padding: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen.menu .button[data-value="contact"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Narrow display */
|
|
||||||
@media (max-width: 300px) {
|
|
||||||
.button svg:not(.hidden) ~ p,
|
|
||||||
header .logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Super-narrow display */
|
|
||||||
@media (max-width: 230px) {
|
|
||||||
header {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .spacer,
|
|
||||||
header p {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Media queries > Media features -- */
|
|
||||||
|
|
||||||
@media (pointer: fine) {
|
|
||||||
.button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (any-hover: hover) {
|
|
||||||
.button {
|
|
||||||
transition: var(--transition) background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.phantom:hover {
|
|
||||||
background-color: rgba(var(--palette-inverted),.2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0l-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg>
|
|
Before Width: | Height: | Size: 274 B |
|
@ -1 +0,0 @@
|
||||||
<svg version="1.1" id="katman_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 1024 1024" xml:space="preserve"><style>.st0{fill:#3a76f0}</style><path class="st0" d="M28.4 241.6v540.7c0 117.7 95.5 213.2 213.2 213.2h540.7c117.7 0 213.2-95.5 213.2-213.2V241.6c0-117.7-95.5-213.2-213.2-213.2H241.6c-117.7 0-213.2 95.5-213.2 213.2z"/><path d="M430.1 180.9l7.7 31c-30.2 7.4-59.1 19.4-85.7 35.4L335.7 220c29.3-17.7 61.2-30.9 94.4-39.1zm163.8 0l-7.7 31c30.2 7.4 59.1 19.4 85.7 35.4l16.5-27.4c-29.4-17.6-61.2-30.8-94.5-39zM220 335.7c-17.7 29.3-30.9 61.2-39.1 94.4l31 7.7c7.4-30.2 19.4-59.1 35.4-85.7L220 335.7zM202.9 512c0-15.5 1.1-31 3.5-46.3l-31.6-4.8c-5.1 33.9-5.1 68.3 0 102.2l31.6-4.8c-2.3-15.3-3.5-30.8-3.5-46.3zm485.4 292l-16.4-27.4c-26.6 16.1-55.4 28-85.6 35.4l7.7 31c33.2-8.2 65-21.3 94.3-39zm132.8-292c0 15.5-1.1 31-3.5 46.3l31.6 4.8c5.1-33.9 5.1-68.3 0-102.2l-31.6 4.8c2.3 15.3 3.5 30.8 3.5 46.3zm22 81.8l-31-7.7c-7.4 30.2-19.4 59.1-35.4 85.7l27.4 16.5c17.6-29.3 30.8-61.2 39-94.5zM558.3 817.6c-30.7 4.6-61.9 4.6-92.6 0l-4.8 31.6c33.9 5.1 68.3 5.1 102.2 0l-4.8-31.6zm202.5-122.3c-18.4 25-40.5 47-65.5 65.4l19 25.7c27.6-20.3 51.9-44.5 72.3-72l-25.8-19.1zm-65.5-432.1c25 18.4 47.1 40.5 65.5 65.5l25.7-19.2c-20.3-27.5-44.6-51.8-72-72l-19.2 25.7zm-432.1 65.5c18.4-25 40.5-47.1 65.5-65.5l-19.2-25.7c-27.5 20.3-51.8 44.6-72 72l25.7 19.2zm540.8 7l-27.4 16.4c16.1 26.6 28 55.4 35.4 85.6l31-7.7c-8.1-33.2-21.3-65-39-94.3zM465.7 206.4c30.7-4.6 61.9-4.6 92.6 0l4.8-31.6c-33.9-5.1-68.3-5.1-102.2 0l4.8 31.6zM279.6 795l-66 15.4 15.4-66-31.1-7.3-15.4 66c-2.5 10.8.7 22.1 8.5 29.9s19.1 11 29.9 8.5l66-15.1-7.3-31.4zm-75.1-86.5l31.1 7.2 10.7-45.8c-15.5-26.1-27.1-54.4-34.4-83.9l-31 7.7c7 28.3 17.5 55.5 31.4 81l-7.8 33.8zm149.2 69.3L308 788.5l7.2 31.1 33.7-7.8c25.6 13.9 52.8 24.5 81.1 31.4l7.7-31c-29.4-7.3-57.6-19-83.7-34.6l-.3.2zM512 234.9c-100.8.1-193.7 54.9-242.4 143.1s-45.7 196 8 281.4L251 773.1l113.7-26.6c99.7 62.8 228.2 55.7 320.4-17.7S812.9 531.7 774 420.5c-39.2-111.3-144.2-185.7-262-185.6z" fill="#fff"/></svg>
|
|
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 671 KiB |
Before Width: | Height: | Size: 786 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 226 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 49 B |
|
@ -1,8 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
$gif = imagecreatefromgif("./pattern.gif");
|
|
||||||
|
|
||||||
header("Content-Type: image/gif");
|
|
||||||
|
|
||||||
imagegif($gif);
|
|
||||||
imagedestroy($gif);
|
|
Before Width: | Height: | Size: 5 MiB |
Before Width: | Height: | Size: 4.2 MiB |
Before Width: | Height: | Size: 721 KiB |
|
@ -1,51 +0,0 @@
|
||||||
// Victor Westerlund - www.victorwesterlund.com
|
|
||||||
|
|
||||||
// UI component constructor
|
|
||||||
class Component {
|
|
||||||
constructor(tag) {
|
|
||||||
this.element = document.createElement(tag); // Root element
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⬇ UI Components ⬇
|
|
||||||
|
|
||||||
export class Button extends Component {
|
|
||||||
constructor(properties) {
|
|
||||||
super("div");
|
|
||||||
this.properties = properties;
|
|
||||||
this.element.classList.add("button");
|
|
||||||
|
|
||||||
this.setText();
|
|
||||||
this.setAction();
|
|
||||||
this.setType();
|
|
||||||
}
|
|
||||||
|
|
||||||
setText() {
|
|
||||||
if(!this.properties.text) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const textElement = document.createElement("p");
|
|
||||||
textElement.innerText = this.properties.text;
|
|
||||||
this.element.appendChild(textElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAction() {
|
|
||||||
if(!this.properties.action) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.element.setAttribute("data-action",this.properties.action);
|
|
||||||
}
|
|
||||||
|
|
||||||
setType() {
|
|
||||||
const types = [
|
|
||||||
"solid",
|
|
||||||
"phantom"
|
|
||||||
];
|
|
||||||
const type = types.includes(this.properties.type) ? this.properties.type : false;
|
|
||||||
|
|
||||||
if(!this.properties.type || !type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.element.classList.add(type);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright © Victor Westerlund - No libraries! 😲
|
|
||||||
|
|
||||||
class Debug {
|
|
||||||
constructor() {
|
|
||||||
console.log("Debug mode is enabled.\nList debug functions by running window._debug.list()");
|
|
||||||
}
|
|
||||||
|
|
||||||
list() {
|
|
||||||
const functions = [
|
|
||||||
"list",
|
|
||||||
"toggleMenu",
|
|
||||||
"openContactsModal",
|
|
||||||
"invalidCard",
|
|
||||||
"infiniteLoadingCard"
|
|
||||||
];
|
|
||||||
console.log("Available functions:",functions.map(f => `window._debug.${f}();`));
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMenu() {
|
|
||||||
document.getElementsByClassName("hamburger")[0].click();
|
|
||||||
}
|
|
||||||
|
|
||||||
openContactsModal() {
|
|
||||||
document.getElementsByClassName("hamburger")[0].click();
|
|
||||||
document.querySelector("div[data-action='openContactCard']").click();
|
|
||||||
}
|
|
||||||
|
|
||||||
demoCard() {
|
|
||||||
const module = import("./Modals.mjs");
|
|
||||||
const interactions = {
|
|
||||||
hello: () => {
|
|
||||||
console.log("Hello world");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.then(modals => {
|
|
||||||
const card = new modals.Card(interactions);
|
|
||||||
card.inner.style.height = "80vh";
|
|
||||||
card.inner.insertAdjacentHTML("afterbegin","<p>Hello world</p>");
|
|
||||||
card.open();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidCard() {
|
|
||||||
const module = import("./Modals.mjs");
|
|
||||||
const interactions = {
|
|
||||||
hello: () => {
|
|
||||||
console.log("Hello world");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.then(modals => {
|
|
||||||
const card = new modals.Card(interactions);
|
|
||||||
card.openPage("invalid_card");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
infiniteLoadingCard() {
|
|
||||||
const module = import("./Modals.mjs");
|
|
||||||
const spinner = document.createElement("div");
|
|
||||||
spinner.classList = "logo spinner";
|
|
||||||
|
|
||||||
module.then(modals => {
|
|
||||||
const card = new modals.Card(new Object());
|
|
||||||
card.insertElement(spinner);
|
|
||||||
card.open();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default window._debug = new Debug();
|
|
|
@ -1,34 +0,0 @@
|
||||||
// Victor Westerlund - www.victorwesterlund.com
|
|
||||||
|
|
||||||
class Logging {
|
|
||||||
constructor() {
|
|
||||||
this.endpoint = "/log/";
|
|
||||||
this.data = new URLSearchParams();
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange",() => {
|
|
||||||
if(document.visibilityState === "hidden") {
|
|
||||||
this.send();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.log("foo","bar");
|
|
||||||
}
|
|
||||||
|
|
||||||
log(key,value) {
|
|
||||||
this.data.append(key,value);
|
|
||||||
}
|
|
||||||
|
|
||||||
send() {
|
|
||||||
const send = navigator.sendBeacon(this.endpoint,this.data);
|
|
||||||
if(send !== true) {
|
|
||||||
const url = this.endpoint + this.data.toString();
|
|
||||||
fetch(url).catch(response => console.log(response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Log {
|
|
||||||
constructor(value,key = "u") {
|
|
||||||
// WIP
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
// Victor Westerlund - www.victorwesterlund.com
|
|
||||||
|
|
||||||
import { default as Interaction, destroy } from "./UI.mjs";
|
|
||||||
import { Button } from "./Components.mjs";
|
|
||||||
|
|
||||||
// Boilerplate for creating element overlays
|
|
||||||
class Modal extends Interaction {
|
|
||||||
constructor(extendedInteractions = {}) {
|
|
||||||
const element = document.createElement("div");
|
|
||||||
let interactions = {
|
|
||||||
close: () => {
|
|
||||||
this.close();
|
|
||||||
},
|
|
||||||
openPage: (event) => {
|
|
||||||
let modal = undefined;
|
|
||||||
switch(event.target.dataset.type) {
|
|
||||||
case "card":
|
|
||||||
modal = new Card({});
|
|
||||||
break;
|
|
||||||
case "dialog":
|
|
||||||
default:
|
|
||||||
modal = new Dialog({});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
modal.openPage(event.target.dataset.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Combine template and incoming interactions into one object
|
|
||||||
interactions = Object.assign(interactions,extendedInteractions);
|
|
||||||
super(interactions,element);
|
|
||||||
|
|
||||||
this.transition = 300;
|
|
||||||
|
|
||||||
this.element = this.applyTemplate(element);
|
|
||||||
this.element.close = () => this.close(); // Bind modal close to element prototype
|
|
||||||
document.body.insertAdjacentElement("afterbegin",this.element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch page html from "assets/pages"
|
|
||||||
async getPage(page) {
|
|
||||||
const url = `assets/pages/${page}`;
|
|
||||||
const response = await fetch(url);
|
|
||||||
if(!response.ok) {
|
|
||||||
const report = {
|
|
||||||
"self": "Modal.getPage()",
|
|
||||||
"self_page": page,
|
|
||||||
"resp_status": response.status,
|
|
||||||
"resp_statusText": response.statusText,
|
|
||||||
"resp_url": response.url,
|
|
||||||
"rqst_ua": navigator.userAgent
|
|
||||||
};
|
|
||||||
throw new Error(JSON.stringify(report,null,2));
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
insertHTML(element) {
|
|
||||||
this.inner.insertAdjacentHTML("afterbegin",element);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertElement(element) {
|
|
||||||
this.inner.insertAdjacentElement("afterbegin",element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply a modal template to the provided element
|
|
||||||
applyTemplate(element) {
|
|
||||||
// The inner div will contain modal content
|
|
||||||
this.inner = document.createElement("div");
|
|
||||||
this.inner.classList.add("inner");
|
|
||||||
element.appendChild(this.inner);
|
|
||||||
element.classList.add("modal");
|
|
||||||
|
|
||||||
// PointerEvents on the outer div will close the modal
|
|
||||||
element.addEventListener("click",event => {
|
|
||||||
if(event.target == element) {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
error(message) {
|
|
||||||
const oops = document.createElement("p");
|
|
||||||
const infoButton = document.createElement("p");
|
|
||||||
|
|
||||||
oops.classList.add("error");
|
|
||||||
oops.innerText = "🤯\nSomething went wrong";
|
|
||||||
|
|
||||||
infoButton.innerText = "more info..";
|
|
||||||
infoButton.addEventListener("click",() => {
|
|
||||||
const details = new Dialog();
|
|
||||||
|
|
||||||
details.insertHTML(`<h1>📄 Error report</h1><pre>${message}</pre>`);
|
|
||||||
details.open();
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.insertElement(infoButton);
|
|
||||||
this.insertElement(oops);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open page from "assets/pages"
|
|
||||||
openPage(page) {
|
|
||||||
// Show a spinner while fetching
|
|
||||||
const spinner = document.createElement("div");
|
|
||||||
spinner.classList = "logo spinner";
|
|
||||||
this.element.setAttribute("data-page",page);
|
|
||||||
this.insertElement(spinner);
|
|
||||||
this.open();
|
|
||||||
|
|
||||||
// Fetch the requested page
|
|
||||||
this.getPage(page).then(html => {
|
|
||||||
this.insertHTML(html);
|
|
||||||
this.bindAll(this.inner);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
const tryAgain = new Button({
|
|
||||||
text: "try again",
|
|
||||||
type: "solid"
|
|
||||||
});
|
|
||||||
tryAgain.element.addEventListener("click",() => {
|
|
||||||
// Clear and recreate modal structure
|
|
||||||
destroy(this.inner);
|
|
||||||
this.applyTemplate(this.element);
|
|
||||||
this.init();
|
|
||||||
this.insertElement(spinner);
|
|
||||||
// Attempt to fetch the requested url again (with soft rate-limiting)
|
|
||||||
setTimeout(() => {
|
|
||||||
this.openPage(page);
|
|
||||||
destroy(spinner);
|
|
||||||
},500);
|
|
||||||
});
|
|
||||||
this.insertElement(tryAgain.element);
|
|
||||||
this.error(error);
|
|
||||||
})
|
|
||||||
.finally(() => destroy(spinner));
|
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
setTimeout(() => this.element.classList.add("active"),this.transition / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the modal and remove it from the DOM
|
|
||||||
close() {
|
|
||||||
this.element.classList.remove("active");
|
|
||||||
setTimeout(() => destroy(this.element),this.transition + 1); // Wait for transition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Dialog extends Modal {
|
|
||||||
constructor(interactions = {}) {
|
|
||||||
super(interactions);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.element.classList.add("dialog");
|
|
||||||
this.element.classList.add("center");
|
|
||||||
const closeButton = new Button({
|
|
||||||
text: "close",
|
|
||||||
action: "close",
|
|
||||||
type: "phantom"
|
|
||||||
});
|
|
||||||
|
|
||||||
this.bind(closeButton.element);
|
|
||||||
this.inner.appendChild(closeButton.element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlay with a slide-in animation from the bottom of the viewport
|
|
||||||
export class Card extends Modal {
|
|
||||||
constructor(interactions = {}) {
|
|
||||||
super(interactions);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.element.classList.add("card");
|
|
||||||
this.element.classList.add("center");
|
|
||||||
const closeButton = new Button({
|
|
||||||
text: "close",
|
|
||||||
action: "close",
|
|
||||||
type: "phantom"
|
|
||||||
});
|
|
||||||
|
|
||||||
this.bind(closeButton.element);
|
|
||||||
this.inner.appendChild(closeButton.element);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
// Victor Westerlund - www.victorwesterlund.com
|
|
||||||
|
|
||||||
// Load assets for later use on this page.
|
|
||||||
// This implements a hybrid of the link types "preload" and "prefetch"
|
|
||||||
export default class Preload {
|
|
||||||
constructor(assets) {
|
|
||||||
this.scripts = [];
|
|
||||||
this.stylesheets = [];
|
|
||||||
|
|
||||||
// Get the type of asset from the file extension
|
|
||||||
assets.forEach(asset => {
|
|
||||||
const components = asset.split(".");
|
|
||||||
const extension = components[components.length - 1];
|
|
||||||
switch(extension) {
|
|
||||||
case "mjs":
|
|
||||||
this.scripts.push(asset);
|
|
||||||
break;
|
|
||||||
case "css":
|
|
||||||
this.stylesheets.push(asset);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Append tags when DOM is ready
|
|
||||||
window.addEventListener("DOMContentLoaded",() => this.import());
|
|
||||||
}
|
|
||||||
|
|
||||||
import() {
|
|
||||||
this.scripts.forEach(script => {
|
|
||||||
const element = document.createElement("script");
|
|
||||||
element.setAttribute("type","module");
|
|
||||||
element.src = "assets/js/" + script;
|
|
||||||
document.body.appendChild(element);
|
|
||||||
});
|
|
||||||
this.stylesheets.forEach(sheet => {
|
|
||||||
const element = document.createElement("link");
|
|
||||||
element.setAttribute("rel","stylesheet");
|
|
||||||
element.href = "assets/css/" + sheet;
|
|
||||||
document.head.appendChild(element);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// Victor Westerlund - www.victorwesterlund.com
|
|
||||||
|
|
||||||
import { default as Logging } from "./Logging.mjs";
|
|
||||||
|
|
||||||
// Remove an element and its subtree
|
|
||||||
export function destroy(family) {
|
|
||||||
while(family.firstChild) {
|
|
||||||
family.removeChild(family.lastChild);
|
|
||||||
}
|
|
||||||
family.parentNode.removeChild(family);
|
|
||||||
}
|
|
||||||
|
|
||||||
// General-purpose scoped event handler
|
|
||||||
export default class Interaction extends Logging {
|
|
||||||
constructor(interactions,scope) {
|
|
||||||
super();
|
|
||||||
this.interactions = interactions;
|
|
||||||
this.attribute = "data-action"; // Target elements with this attribute
|
|
||||||
|
|
||||||
this.bindAll(scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind event listeners to this element
|
|
||||||
bind(element) {
|
|
||||||
if(element.hasAttribute("data-bound") || !element.hasAttribute(this.attribute)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
element.addEventListener("click",event => this.pointerEvent(event));
|
|
||||||
element.setAttribute("data-bound","");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all elements with the target attribute in scope
|
|
||||||
getAll(scope) {
|
|
||||||
return scope.querySelectorAll(`[${this.attribute}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind listeners to all attributed elements within scope
|
|
||||||
bindAll(scope) {
|
|
||||||
const elements = this.getAll(scope);
|
|
||||||
for(const element of elements) {
|
|
||||||
this.bind(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle click/touch interactions
|
|
||||||
pointerEvent(event) {
|
|
||||||
const target = event.target.closest(`[${this.attribute}]`);
|
|
||||||
const action = target?.getAttribute(this.attribute) ?? null;
|
|
||||||
|
|
||||||
if(!target || !action || !Object.keys(this.interactions).includes(action)) {
|
|
||||||
// Exit if the interaction is invalid or action doesn't exist
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Execute the function from the data-action attribute
|
|
||||||
this.interactions[action](event);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// Promote IE to Edge
|
|
||||||
if(/MSIE \d|Trident.*rv:/.test(navigator.userAgent)) {
|
|
||||||
window.location = 'microsoft-edge:' + window.location;
|
|
||||||
setTimeout(function() {
|
|
||||||
window.location = 'https://go.microsoft.com/fwlink/?linkid=2135547';
|
|
||||||
},1);
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
// Victor Westerlund - www.victorwesterlund.com
|
|
||||||
import { default as Preload } from "./modules/Preload.mjs";
|
|
||||||
import { default as Interaction, destroy } from "./modules/UI.mjs";
|
|
||||||
|
|
||||||
// Load these assets when the DOM is ready (not needed right away)
|
|
||||||
new Preload([
|
|
||||||
"modules/Modals.mjs",
|
|
||||||
"modules/Components.mjs",
|
|
||||||
"modal.css"
|
|
||||||
]);
|
|
||||||
|
|
||||||
function updateTheme() {
|
|
||||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
document.body.classList.remove("dark");
|
|
||||||
|
|
||||||
// Force dark theme on all pages
|
|
||||||
if(media.matches) {
|
|
||||||
document.body.classList.add("dark");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All default interactions
|
|
||||||
const interactions = {
|
|
||||||
toggleMenu: () => {
|
|
||||||
const transition = 200;
|
|
||||||
const menu = document.getElementsByTagName("main")[0];
|
|
||||||
|
|
||||||
// Animate menu state change
|
|
||||||
menu.style.setProperty("transition",`${transition}ms`);
|
|
||||||
document.body.classList.toggle("menuActive");
|
|
||||||
// Remove transition CSS when finished. Wonky resize effects otherwise
|
|
||||||
setTimeout(() => menu.style.removeProperty("transition"),transition + 1);
|
|
||||||
},
|
|
||||||
// Open page defined with data-value as a card
|
|
||||||
newCard: (event) => {
|
|
||||||
const module = import("./modules/Modals.mjs");
|
|
||||||
const interactions = {
|
|
||||||
// Like newCard() except it closes the previous card
|
|
||||||
getContact: (event) => {
|
|
||||||
const service = event.target.dataset.value;
|
|
||||||
module.then(modals => {
|
|
||||||
event.target.closest(".modal").close();
|
|
||||||
const card = new modals.Card(interactions);
|
|
||||||
card.openPage(service);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// Copy text defined in data-value to clipboard and play animation
|
|
||||||
copyText: (event) => {
|
|
||||||
const copy = navigator.clipboard.writeText(event.target.dataset.value);
|
|
||||||
copy.then(() => {
|
|
||||||
event.target.classList.add("copied");
|
|
||||||
const copied = document.createElement("p");
|
|
||||||
copied.innerText = "copied!";
|
|
||||||
event.target.appendChild(copied);
|
|
||||||
|
|
||||||
// Reset button state
|
|
||||||
setTimeout(() => {
|
|
||||||
event.target.classList.remove("copied");
|
|
||||||
destroy(copied);
|
|
||||||
},1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create card and open the specified page asynchronously
|
|
||||||
module.then(modals => {
|
|
||||||
const card = new modals.Card(interactions);
|
|
||||||
card.openPage(event.target.dataset.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.serviceWorker.getRegistrations().then(serviceWorkers => {
|
|
||||||
for(const serviceWorker of serviceWorkers) {
|
|
||||||
serviceWorker.unregister();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the current page theme, and listen for changes
|
|
||||||
const theme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
theme.addEventListener("change",updateTheme);
|
|
||||||
|
|
||||||
new Interaction(interactions,document.body); // Initialize default interactions
|
|
||||||
updateTheme();
|
|
|
@ -1,45 +0,0 @@
|
||||||
<!-- Victor Westerlund - www.victorwesterlund.com -->
|
|
||||||
<style>
|
|
||||||
.contact {
|
|
||||||
align-self: stretch;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact .item {
|
|
||||||
--size: 1fr;
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background-color: rgba(var(--palette-inverted),.05);
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact .item * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact .item img {
|
|
||||||
height: 8vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 300px) {
|
|
||||||
.contact {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="contact">
|
|
||||||
<div class="item button phantom" data-action="getContact" data-value="contact_signal">
|
|
||||||
<img src="assets/img/icons/signal.svg"/>
|
|
||||||
<p>Signal</p>
|
|
||||||
</div>
|
|
||||||
<div class="item button phantom" data-action="getContact" data-value="contact_email">
|
|
||||||
<img src="assets/img/icons/email.svg"/>
|
|
||||||
<p>E-Mail</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,65 +0,0 @@
|
||||||
<!-- Victor Westerlund - www.victorwesterlund.com -->
|
|
||||||
<style>
|
|
||||||
.button.copied {
|
|
||||||
pointer-events: none;
|
|
||||||
animation: beat 500ms forwards;
|
|
||||||
animation-delay: 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.copied svg,
|
|
||||||
.button.copied p:first-of-type {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.copied p:last-of-type {
|
|
||||||
position: absolute;
|
|
||||||
animation: slide 1000ms forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes beat {
|
|
||||||
0% { transform: scale(1); }
|
|
||||||
25% { transform: scale(.95); }
|
|
||||||
55% { transform: scale(1.05); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide {
|
|
||||||
0% { transform: translateY(calc(var(--padding))); opacity: 0; }
|
|
||||||
30% { transform: translateY(0); opacity: 1; }
|
|
||||||
70% { transform: translateY(0); opacity: 1; }
|
|
||||||
100% { transform: translateY(calc(var(--padding) * -1)); opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
a.button {
|
|
||||||
color: var(--swatch-accent);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.button::before {
|
|
||||||
content: "tap ";
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 300px) {
|
|
||||||
.button.copied p:last-of-type {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (pointer: fine) {
|
|
||||||
a.button::before {
|
|
||||||
content: "click ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="button phantom" data-action="openPage" data-value="contact_email_pgp" data-type="card">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
|
|
||||||
<p>PGP key</p>
|
|
||||||
</div>
|
|
||||||
<p></p>
|
|
||||||
<h1>hello@victorwesterlund.com</h1>
|
|
||||||
<p>You can also <a class="button" href="mailto:hello@victorwesterlund.com?subject=Hello Victor!">here</a> to send a mail directly from your mail app</p>
|
|
||||||
<p></p>
|
|
||||||
<div class="button solid" data-action="copyText" data-value="hello@victorwesterlund.com">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18 2H9c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H9V4h9v12zM3 15v-2h2v2H3zm0-5.5h2v2H3v-2zM10 20h2v2h-2v-2zm-7-1.5v-2h2v2H3zM5 22c-1.1 0-2-.9-2-2h2v2zm3.5 0h-2v-2h2v2zm5 0v-2h2c0 1.1-.9 2-2 2zM5 6v2H3c0-1.1.9-2 2-2z"/></svg>
|
|
||||||
<p>copy email</p>
|
|
||||||
</div>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<!-- Victor Westerlund - www.victorwesterlund.com -->
|
|
||||||
<h1>🔑 PGP Public Key</h1>
|
|
||||||
<p></p>
|
|
||||||
<p>5466 B1EB 2F44 6D3D DC34 E9F7 5BE0 CB0B E3BB 69DA</p>
|
|
||||||
<p class="button" data-action="openPage" data-value="contact_email_pgp_view" data-type="dialog">show key</p>
|
|
||||||
<a href="https://storage.googleapis.com/public.victorwesterlund.com/publickey.gpg"><div class="button solid">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z"/></svg>
|
|
||||||
<p>download .gpg</p>
|
|
||||||
</div></a>
|
|
|
@ -1,31 +0,0 @@
|
||||||
<!-- Victor Westerlund - www.victorwesterlund.com -->
|
|
||||||
<pre>
|
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mQENBF/K6MkBCACkRMhMfYdeNP+M3XQoZHQVJgippQvYZ4QqH6F6brWD5989Xy5W
|
|
||||||
kDCvLbmPJ66boqB0dHExswOvMlhfFha65pRmfP6lIoIxZlZKwll1XASP2osS8f6r
|
|
||||||
63T7hAbL3V2Dkm49tiH1tk578xGomDrxOrd4izpH4mn9AyBIL4M+5j34bKFVZKQ+
|
|
||||||
QfMu7tduF/1oQHfDaXJeLXSfn5cNTy8DlLcLJKUSk4cjabf1D88gMVszqAAC5o1a
|
|
||||||
fI0YxoyZ+Fv+CmyrQm2iIZ3+MyDU9JAvoImtlp1h5aNgbFRDi2vKcSlv158Hq97Z
|
|
||||||
XlH1ttRZuFZiJzb8iukgUUFi4RORoXWt2rtNABEBAAG0LlZpY3RvciBXZXN0ZXJs
|
|
||||||
dW5kIDxoZWxsb0B2aWN0b3J3ZXN0ZXJsdW5kLmNvbT6JATUEEAEIACkFAl/K6MkG
|
|
||||||
CwkIBwMCCRBb4MsL47tp2gQVCAIKAxYCAQIZAQIbAwIeAQAAJ5MIAKDl9yHjwTO7
|
|
||||||
20sDrPa6ECsSBU/FwkvkWecuauvY19/OqtacNk8dEeiITLeUeBXkvNzN+P0y8hoF
|
|
||||||
ABZeir59dsY00iIp8gm03eLalhcblR5jYe3c08HssJH8PksczP3kitRNLvPAf2nU
|
|
||||||
BYg3zca5Ka21/4BPRLFb9SAQGxfHyZdy3Poug+o+pokbeK2wLqqfSMtH+waBB6Lg
|
|
||||||
2dRXuEnaZorUpNBpsahxastvNehv31Ke41Brvft15VKpO25GKZDPhm0odXMth1/J
|
|
||||||
pzWRQtndazY2guB0Ft+5wujv28HFCgVgZn2fKiQVytAetO+/wzPijBkGRvdIE+Zb
|
|
||||||
VRd3Nc0mHI65AQ0EX8royQEIALcoWEurmyXD2LoGvR+sYW+YPAPM6KG8KF4cWUn8
|
|
||||||
8+kZ6F4FH9OW64di2npYe3x+zR7DgQ1yHXcmalAsP0nN4JWTavLwsSO+JAv8NpL5
|
|
||||||
bgDs6fGaEQFl+X4fYOpkBkBmb1JrbnBk1a2u3qsEw8t7+wW1LG9z/Si5+G1KQko8
|
|
||||||
x/PEaZ2ZVv7L51ZfIQRnMtl4vL5X23BPVsDywotvuFqlTiSjGP4CR0lVa5CRv3DJ
|
|
||||||
FSmHxAxeI0vMMlwbIIUTrtwJR320sZvh2cRiwAXHQXm6l0ojzRnl46mmXnB3N6q9
|
|
||||||
PyWOaUgPrMFjT24wtgopIOwbFAT3xTr1Un0FbdeaG9JhdJ8AEQEAAYkBHwQYAQgA
|
|
||||||
EwUCX8royQkQW+DLC+O7adoCGwwAAIV/B/9OLYeQOxbXh1/hvW7/oTvN1py8wfFq
|
|
||||||
buvQSrb/MZKm6lZgG+kQy3DWjGTi/xvNqDHfBiObFSGso8RHSbHFldzEuMgrgoWW
|
|
||||||
/4JH1GDiKOp+rmBxfG30/DzOoFSfVcUfP5r8xNQby4Bh6zJhKPKVB3sZjO8cHNZD
|
|
||||||
HcNAqT3Gh5yFzsUna+ZjvPF7iU5RF1YP46dsIdvuo4xFbHpEPoZs7wgZijf+vmKO
|
|
||||||
lP61UFvKuXzwcLiI6s919EBJ9+7je8ZAxe6BCaazk+AhxXeokVvDgwQ150DNk4up
|
|
||||||
1ftWZI0LHqEpVGNejQ09uu+TdC/ISy/Ti0XKlJDER1eUL577YRUl876Y
|
|
||||||
=2qWm
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----</pre>
|
|
|
@ -1,76 +0,0 @@
|
||||||
<!-- Victor Westerlund - www.victorwesterlund.com -->
|
|
||||||
<style>
|
|
||||||
body:not(.dark) .modal[data-page="contact_signal"] .inner {
|
|
||||||
--palette-inverted: 255,255,255;
|
|
||||||
--palette-background: 58,118,240;
|
|
||||||
|
|
||||||
--swatch-background: rgb(var(--palette-background));
|
|
||||||
--swatch-inverted: rgb(var(--palette-inverted));
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.dark) .modal[data-page="contact_signal"] .button.solid {
|
|
||||||
background-color: var(--swatch-inverted);
|
|
||||||
color: var(--swatch-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .inner > h1,
|
|
||||||
.modal .inner > p,
|
|
||||||
.modal .button.phantom {
|
|
||||||
color: var(--swatch-inverted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- */
|
|
||||||
|
|
||||||
#logo_signal {
|
|
||||||
width: clamp(100px,50%,200px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#number {
|
|
||||||
background: black;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.copied {
|
|
||||||
pointer-events: none;
|
|
||||||
animation: beat 500ms forwards;
|
|
||||||
animation-delay: 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.copied svg,
|
|
||||||
.button.copied p:first-of-type {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.copied p:last-of-type {
|
|
||||||
position: absolute;
|
|
||||||
animation: slide 1000ms forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes beat {
|
|
||||||
0% { transform: scale(1); }
|
|
||||||
25% { transform: scale(.95); }
|
|
||||||
55% { transform: scale(1.05); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide {
|
|
||||||
0% { transform: translateY(calc(var(--padding))); opacity: 0; }
|
|
||||||
30% { transform: translateY(0); opacity: 1; }
|
|
||||||
70% { transform: translateY(0); opacity: 1; }
|
|
||||||
100% { transform: translateY(calc(var(--padding) * -1)); opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 300px) {
|
|
||||||
.button.copied p:last-of-type {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<img id="logo_signal" src="assets/img/icons/signal.svg"/>
|
|
||||||
<h1 id="number">+4670-245-2459</h1>
|
|
||||||
<p>Signal is a free and encrypted message platform with apps for all major platforms.</p>
|
|
||||||
<div class="button solid" data-action="copyText" data-value="+46702452459">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18 2H9c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H9V4h9v12zM3 15v-2h2v2H3zm0-5.5h2v2H3v-2zM10 20h2v2h-2v-2zm-7-1.5v-2h2v2H3zM5 22c-1.1 0-2-.9-2-2h2v2zm3.5 0h-2v-2h2v2zm5 0v-2h2c0 1.1-.9 2-2 2zM5 6v2H3c0-1.1.9-2 2-2z"/></svg>
|
|
||||||
<p>copy number</p>
|
|
||||||
</div>
|
|
|
@ -4,71 +4,15 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Victor Westerlund</title>
|
<title>Victor Westerlund</title>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Full-stack web developer from Stockholm, Sweden.">
|
<meta name="description" content="Full-stack web developer from Stockholm, Sweden.">
|
||||||
|
|
||||||
<link rel="icon" href="assets/img/favicon.png">
|
<link rel="icon" href="assets/img/favicon.png">
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<div class="screen landingpage">
|
|
||||||
<header>
|
|
||||||
<div class="hamburger center" data-action="toggleMenu" data-theme-color="contrast">
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<div class="logo"></div>
|
|
||||||
<p>victor westerlund</p>
|
<p>victor westerlund</p>
|
||||||
</header>
|
<a href="https://github.com/VictorWesterlund"><p>github →</p></a>
|
||||||
<div class="content center">
|
|
||||||
<div class="pattern center">
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<picture>
|
|
||||||
<source srcset="assets/img/myface/highres.avif" type="image/avif" media="(min-width: 1920px)">
|
|
||||||
<source srcset="assets/img/myface/highres.webp" type="image/webp" media="(min-width: 1920px)">
|
|
||||||
<source srcset="assets/img/myface/mediumres.avif" type="image/avif">
|
|
||||||
<source srcset="assets/img/myface/mediumres.webp" type="image/webp">
|
|
||||||
<img src="assets/img/myface/mediumres.png" type="image/png">
|
|
||||||
</picture>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="screen menu dark">
|
|
||||||
<header>
|
|
||||||
<div class="hamburger center" data-action="toggleMenu" data-theme-color="background">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="28.548" height="22.828"><path d="M2.28 11.414h25.269M1.414 11.414l10-10M1.414 11.414l10 10"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<div class="logo"></div>
|
|
||||||
<p>victor westerlund</p>
|
|
||||||
</header>
|
|
||||||
<div class="content">
|
|
||||||
<div class="wide">
|
|
||||||
<div class="logo"></div>
|
|
||||||
<div class="group">
|
|
||||||
<h1><span>victor westerlund</span></h1>
|
|
||||||
<p>I create things with code. The things I've created for the public reside as open-source repositories on GitHub, the rest you might hear about from me some day.</p>
|
|
||||||
<p>Other topics (seemingly irrelevant to programming) I find facinating include but is in no way limited to astronomy, psychology, sociology, economics, ...</p>
|
|
||||||
</div>
|
|
||||||
<nav>
|
|
||||||
<p class="button" data-action="newCard" data-value="contact_signal">signal</p>
|
|
||||||
<p class="button" data-action="newCard" data-value="contact_email">email</p>
|
|
||||||
<a href="https://github.com/VictorWesterlund" target="_blank"><p class="button">github →</p></a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="narrow center">
|
|
||||||
<p>I create things with code. The things I've created for the public reside as open-source repositories on GitHub, the rest you might hear about from me some day.</p>
|
|
||||||
<p>Other topics (seemingly irrelevant to programming) I find facinating include but is in no way limited to astronomy, psychology, sociology, economics, ...</p>
|
|
||||||
</div>
|
|
||||||
<div class="button phantom" data-action="newCard" data-value="contact">
|
|
||||||
<p>contact me</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="assets/js/script.js"></script>
|
|
||||||
<script nomodule src="assets/js/nomodule.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|