Compare commits

...

14 commits

Author SHA1 Message Date
48e9914215 build: 📦 update packages 2025-06-20 22:45:20 +02:00
7280829591 feat: add nsfw barrier component 2025-06-20 22:44:59 +02:00
76473e5a78 fix: 🍱 update clothing ref image
Major historical events attach connotations to wearing the scarf that the owner does not share
2025-06-20 21:36:24 +02:00
251560baf1 feat: 🍱 add nsfw ref images 2025-06-20 21:32:15 +02:00
05bae8229f refactor: ♻️ modularize button gradients
This allows for more flexibility when recoloring buttons
2025-06-20 21:30:26 +02:00
5a3b2ac3d4 fix: 🐛 calculate quick info gradient from base color 2025-06-20 21:21:38 +02:00
45337e216b feat: add image switching 2025-06-20 17:17:42 +02:00
fa19ee5e91 refactor: ♻️ use custom property for gradient direction 2025-06-20 15:08:46 +02:00
788dc36364 feat: 💄 update button styles of filter list
Harmonize filter list button styles with rest of the page
2025-06-20 14:49:34 +02:00
3a52d4a004 feat: add popup modal component 2025-06-20 14:46:04 +02:00
bf29aa9bd7 feat: add button component 2025-06-20 14:03:38 +02:00
e8f7d61ed0 feat: add wrapper around sessionStorage API
The native 'storage' event does not fire on the same browsing context if a change to the store happens, so wrapping the sessionStorage API is necessary to work around this limitation
2025-06-20 14:01:48 +02:00
Sebin Nyshkim
031d698949 build: 🔧 use incremental mode when serving locally 2025-06-10 17:21:17 +02:00
Sebin Nyshkim
ae5c391d4b build: 📦 update to eleventy v3.1.0 2025-06-10 17:21:17 +02:00
20 changed files with 778 additions and 929 deletions

1142
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "eleventy --serve ",
"start": "eleventy --serve --incremental",
"build": "ELEVENTY_PRODUCTION=true eleventy",
"prebuild": "rm -rf ./public"
},
@ -13,9 +13,9 @@
"description": "",
"type": "module",
"dependencies": {
"@11ty/eleventy": "^3.0.0",
"@11ty/eleventy-img": "^6.0.1",
"@11ty/eleventy-navigation": "^1.0.1",
"@11ty/eleventy": "^3.1.1",
"@11ty/eleventy-img": "^6.0.4",
"@11ty/eleventy-navigation": "^1.0.4",
"@11ty/eleventy-plugin-webc": "^0.11.2"
}
}

View file

@ -32,17 +32,22 @@
:host {
--clr-love: oklch(60% 0.25 10deg);
--clr-yes: oklch(65% 0.2 140deg);
--clr-maybe: oklch(75% 0.15 85deg);
--clr-no: oklch(40% 0.15 30deg);
--clr-maybe: oklch(65% 0.15 70deg);
--clr-no: oklch(40% 0.2 40deg);
--clr-tag: oklch(65% 0.15 245deg);
}
:host {
--gradient-dir: to bottom right;
--gradient-base: var(--clr-box-background);
--gradient-start: oklch(from var(--gradient-base) calc(l + 0.2) c h);
--gradient-end: oklch(from var(--gradient-base) l c h);
position: relative;
background: linear-gradient(
to bottom right,
var(--clr-box-gradient-start) 0%,
var(--clr-box-gradient-end) 50%
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
font-size: 0.75em;
@ -61,13 +66,15 @@
}
:host::before {
--gradient-start: oklch(from var(--gradient-base) calc(l + 0.1) c h);
content: '';
position: absolute;
inset: var(--border-thin);
background: linear-gradient(
to bottom right,
var(--clr-quick-info-bg-start) 0%,
var(--clr-quick-info-bg-end) 50%
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
border-radius: inherit;
z-index: -1;
@ -88,37 +95,55 @@
display: none;
}
:host .filter-button input:checked + label {
--box-shadow-size: 0.25em;
--button-top: 0.25em;
}
:host .filter-button label {
display: block;
--gradient-base: var(--clr-button);
--gradient-start: oklch(from var(--gradient-base) calc(l + 0.2) c h);
--gradient-end: oklch(from var(--gradient-base) l c h);
position: relative;
top: var(--button-top, 0);
display: block;
background-color: var(--clr-button);
background: linear-gradient(
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
font-weight: 700;
text-decoration: none;
color: white;
font-size: 1em;
font-weight: bold;
text-align: center;
color: var(--theme-c-primary-100);
margin: 0.5em 0;
border-radius: 0.25em;
box-shadow: 0.125em 0.125em 0.5em var(--clr-box-shadow);
border: none;
border-radius: 0.5em;
padding: 0.5em 1em;
box-shadow: 0 var(--box-shadow-size, 0.5em) 0 0 oklch(from var(--clr-button) calc(l - 0.3) c h);
overflow: hidden;
z-index: 1;
transition: all 0.1s ease-out;
transition: all 0.2s ease-in-out;
}
:host .filter-button label:hover {
--box-shadow-size: 0.75em;
--button-top: -0.25em;
cursor: pointer;
:host .filter-button label::before {
--gradient-start: oklch(from var(--gradient-base) calc(l + 0.1) c h);
content: '';
position: absolute;
inset: var(--border-thin);
background: linear-gradient(
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
border-radius: inherit;
z-index: -1;
}
:host .filter-button input:checked + label {
--gradient-dir: to top left;
--clr-box-background: oklch(from var(--clr-button) calc(l * 0.825) c h);
}
:host .filter-button label[for='button-love'] {
@ -145,7 +170,7 @@
margin: 0;
padding: 0;
overflow: scroll;
overflow-y: scroll;
}
:host .item {

View file

@ -14,6 +14,8 @@
--border-thin: 0.0625em;
--border-radius: 0.75em;
--timing-func: cubic-bezier(0.68, -0.55, 0.27, 1.55);
@media (min-height: 64em) {
--page-spacing: 3em;
}

View file

@ -45,8 +45,9 @@
--clr-color-box-background: var(--theme-c-primary-200);
--clr-quick-info-bg-start: var(--theme-c-primary-100);
--clr-quick-info-bg-end: var(--theme-c-primary-150);
--clr-quick-info-background: var(--theme-c-primary-150);
--clr-quick-info-gradient-start: oklch(from var(--clr-quick-info-background) calc(l + 0.1) c h);
--clr-quick-info-gradient-end: oklch(from var(--clr-quick-info-background) l c h);
@media (prefers-color-scheme: dark) {
--clr-page-background: var(--theme-c-primary-850);
@ -62,8 +63,7 @@
--clr-color-box-background: var(--theme-c-primary-700);
--clr-quick-info-bg-start: var(--theme-c-primary-700);
--clr-quick-info-bg-end: var(--theme-c-primary-750);
--clr-quick-info-background: var(--theme-c-primary-750);
}
}

View file

@ -0,0 +1,70 @@
<script>
const storageHandler = {
get(target, prop) {
switch (prop) {
case 'setItem':
return function (...args) {
const [key, value] = args;
// Add additional logic here, such as logging or validation
console.log(`Setting key "${key}" to value "${value}"`);
updateDom(key, value);
target[key] = value;
};
case 'getItem':
return function (...args) {
const [key] = args;
// Add additional logic here, such as logging or validation
console.log(`Getting key "${key}"`);
return target[key];
};
case 'removeItem':
return function (...args) {
const [key] = args;
// Add additional logic here, such as logging or validation
console.log(`Removing key "${key}"`);
delete target[key];
};
case 'clear':
return function () {
// Add additional logic here, such as logging or validation
console.log('Clearing all sessionStorage');
for (let key in target) {
if (target.hasOwnProperty(key)) {
delete target[key];
}
}
};
default:
return target[prop];
}
},
set(target, prop, value) {
// Add additional logic here, such as validation or constraints
console.log(`Setting key "${prop}" to value "${value}"`);
target[prop] = value;
return true;
},
deleteProperty(target, prop) {
// Add additional logic here, such as logging or validation
console.log(`Deleting key "${prop}"`);
delete target[prop];
return true;
}
};
const storeProxy = new Proxy(sessionStorage, storageHandler);
const updateDom = (key, value) => {
if (key === 'isHorny') {
value ? document.body.classList.add('nsfw') : document.body.classList.remove('nsfw');
}
};
// Usage example:
storeProxy.getItem('isHorny') === 'true'
? document.body.classList.add('nsfw')
: document.body.classList.remove('nsfw');
</script>

View file

@ -16,6 +16,10 @@
min-height: 100dvh;
}
body.scroll-lock {
overflow: hidden;
}
h1,
h2,
h3,

View file

@ -0,0 +1,44 @@
<script>
const nsfwButton = document.querySelector('#nsfw-toggle');
nsfwButton.addEventListener('click', () => {
const isInHornyJail = storeProxy.getItem('isInHornyJail') === 'true';
const isHorny = storeProxy.getItem('isHorny') === 'true';
if (isInHornyJail) {
storeProxy.setItem('isHorny', !isHorny);
} else {
open();
}
});
</script>
<div webc:root="override">
<div class="message">
<div webc:type="11ty" 11ty:type="md">
<slot name="message"></slot>
</div>
</div>
<ref-button id="nsfw-toggle" command="show-modal" commandfor="nsfw-warning">
Reveal/Hide
</ref-button>
<div class="nsfw-content">
<div webc:type="11ty" 11ty:type="md">
<slot></slot>
</div>
</div>
</div>
<style webc:scoped="nsfw-barrier">
:host {
.nsfw & .nsfw-content {
display: block;
}
}
:host .nsfw-content {
display: none;
}
</style>

View file

@ -0,0 +1,186 @@
<script>
const dialog = document.querySelector('dialog');
const yesButton = document.querySelector('button.positive');
const noButton = document.querySelector('button.negative');
const open = () => {
dialog.showModal();
document.body.classList.toggle('scroll-lock');
document.body.inert = true;
};
const close = (result) => {
document.body.classList.toggle('scroll-lock');
document.body.inert = false;
storeProxy.setItem('isHorny', result);
if (result && storeProxy.getItem('isInHornyJail') !== 'true') {
storeProxy.setItem('isInHornyJail', true);
}
dialog.close();
};
dialog.addEventListener('cancel', (e) => {
e.preventDefault();
close(storeProxy.getItem('isHorny'));
});
yesButton.addEventListener('click', (e) => {
e.preventDefault();
close(true);
});
noButton.addEventListener('click', (e) => {
e.preventDefault();
close(false);
});
</script>
<dialog :id="id" webc:root="override">
<form method="dialog" class="content">
<h2 class="heading">
<slot name="heading"></slot>
</h2>
<div class="message">
<slot name="message"></slot>
</div>
<div class="actions">
<ref-button class="positive">Yes, show me the goods 👀</ref-button>
<ref-button class="negative">NO, STAHP 😱</ref-button>
</div>
</form>
</dialog>
<style webc:scoped="modal">
:host {
--clr-yes: oklch(65% 0.2 140deg);
--clr-no: oklch(40% 0.2 40deg);
}
:host {
display: none;
background: linear-gradient(
to bottom right,
var(--clr-box-gradient-start) 0%,
var(--clr-box-gradient-end) 50%
);
color: var(--clr-text);
width: 30em;
margin: auto;
border: none;
border-radius: 1.5em;
padding: 1rem;
overflow: hidden;
z-index: 1;
opacity: 0;
transition: all 0.75s ease-in-out allow-discrete;
animation: fade-in 0.5s;
animation-timing-function: var(--timing-func);
transition: all 0.5s;
transition-timing-function: var(--timing-func);
}
:host::before {
content: '';
position: absolute;
inset: var(--border-thin);
background-color: var(--clr-box-background);
border-radius: inherit;
z-index: -1;
}
:host[open] {
opacity: 1;
display: block;
}
:host::backdrop {
backdrop-filter: blur(0rem);
background-color: oklch(from black l c h / 0.5);
opacity: 0;
transition: all 0.75s ease-in-out allow-discrete;
animation: backdrop-fade-in 0.5s;
}
:host[open]::backdrop {
backdrop-filter: blur(1rem);
opacity: 1;
}
:host .content {
display: flex;
flex-flow: column nowrap;
justify-content: center;
text-align: center;
gap: 1.5rem;
}
:host .content > * {
flex: 1 1 auto;
}
:host .heading {
margin: 1.875rem 0 0 0;
}
:host .message {
flex: 1 1 100%;
}
:host .actions {
display: flex;
flex-flow: column nowrap;
gap: 1em;
}
:host .positive {
--gradient-base: var(--clr-yes);
}
:host .negative {
--gradient-base: var(--clr-no);
}
@starting-style {
:host {
display: none;
opacity: 0;
}
:host::backdrop {
backdrop-filter: blur(0rem);
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
scale: 0;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes backdrop-fade-in {
from {
backdrop-filter: blur(0rem);
opacity: 0;
}
to {
backdrop-filter: blur(1rem);
opacity: 1;
}
}
</style>

View file

@ -5,11 +5,15 @@
<style webc:scoped="quick-info">
:host {
--gradient-dir: to bottom right;
--gradient-start: var(--clr-box-gradient-start);
--gradient-end: var(--clr-box-gradient-end);
position: relative;
background: linear-gradient(
to bottom right,
var(--clr-box-gradient-start) 0%,
var(--clr-box-gradient-end) 50%
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
box-shadow: 0.125em 0.125em 0.5em var(--clr-box-shadow);
@ -22,13 +26,16 @@
}
:host::before {
--gradient-start: var(--clr-quick-info-gradient-start);
--gradient-end: var(--clr-quick-info-gradient-end);
content: '';
position: absolute;
inset: var(--border-thin);
background: linear-gradient(
to bottom right,
var(--clr-quick-info-bg-start) 0%,
var(--clr-quick-info-bg-end) 50%
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
border-radius: inherit;
z-index: -1;

View file

@ -0,0 +1,50 @@
<button webc:root="override"><slot></slot></button>
<style webc:scoped="button">
:host {
--gradient-dir: to bottom right;
--gradient-base: var(--clr-box-background);
--gradient-start: oklch(from var(--gradient-base) calc(l + 0.2) c h);
--gradient-end: oklch(from var(--gradient-base) l c h);
position: relative;
background: linear-gradient(
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
color: var(--clr-text);
font-size: 1em;
font-weight: bold;
box-shadow: 0.125em 0.125em 0.5em var(--clr-box-shadow);
border: none;
border-radius: 0.5em;
padding: 0.5em 1em;
overflow: hidden;
z-index: 1;
}
:host::before {
--gradient-start: oklch(from var(--gradient-base) calc(l + 0.1) c h);
content: '';
position: absolute;
inset: var(--border-thin);
background: linear-gradient(
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
border-radius: inherit;
z-index: -1;
}
:host:active {
--gradient-dir: to top left;
}
</style>

View file

@ -121,6 +121,10 @@
}
:host :where(.prev, .next, .indicator-btn) {
--gradient-dir: to bottom right;
--gradient-start: var(--clr-box-gradient-start);
--gradient-end: var(--clr-box-gradient-end);
position: relative;
font-size: 1.25em;
@ -128,9 +132,9 @@
cursor: pointer;
background: linear-gradient(
to bottom right,
var(--clr-box-gradient-start) 0%,
var(--clr-box-gradient-end) 50%
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
box-shadow: 0.125em 0.125em 0.5em var(--clr-box-shadow);
@ -143,21 +147,21 @@
}
:host :where(.prev, .next, .indicator-btn):active::after {
background: linear-gradient(
to top left,
var(--clr-quick-info-bg-start) 0%,
var(--clr-quick-info-bg-end) 50%
);
--gradient-dir: to top left;
}
:host :where(.prev, .next, .indicator-btn)::after {
--gradient-dir: to bottom right;
--gradient-start: var(--clr-quick-info-gradient-start);
--gradient-end: var(--clr-quick-info-gradient-end);
content: '';
position: absolute;
inset: var(--border-thin);
background: linear-gradient(
to bottom right,
var(--clr-quick-info-bg-start) 0%,
var(--clr-quick-info-bg-end) 50%
var(--gradient-dir),
var(--gradient-start) 0%,
var(--gradient-end) 50%
);
border-radius: inherit;
z-index: -1;
@ -255,10 +259,6 @@
}
:host .indicator-btn[aria-current='true']::after {
background: linear-gradient(
to top left,
var(--clr-quick-info-bg-start) 0%,
var(--clr-quick-info-bg-end) 50%
);
--gradient-dir: to top left;
}
</style>

View file

@ -7,6 +7,15 @@
:width="width"
sizes="1000px"
></eleventy-image>
<eleventy-image
class="image nsfw"
:class="(dropshadow ? 'dropshadow' : '')"
:src="`src/img/${char}/${src.replace(/^(.*?)(\.[^.]+)$/, '$1-nsfw$2')}`"
:alt="`${alt} by ${artist}`"
:width="width"
sizes="1000px"
webc:if="nsfw"
></eleventy-image>
<figcaption class="caption">
<template webc:nokeep @text="alt"></template>
&copy;
@ -18,19 +27,35 @@
:host {
display: grid;
grid-template-rows: [image-start] 1fr [image-end caption-start] auto [caption-end];
grid-template-columns: [image-start] 1fr [image-end];
place-content: center;
row-gap: 1rem;
margin-block: 0;
margin-inline: auto;
.nsfw &:has(.image.nsfw) {
& .image {
display: none;
}
& .image.nsfw {
display: block;
}
}
.nsfw &:not(:has(.image.nsfw)) {
& .image {
display: block;
}
}
}
:host :first-child {
:host :where(picture, img) {
grid-area: image;
}
:host img {
:host .image {
display: block;
width: 100%;
height: 100%;
@ -39,12 +64,16 @@
border-radius: 1em;
}
:host .image.nsfw {
display: none;
}
:host .dropshadow {
filter: drop-shadow(0.5em 0.25em 0.375em oklch(0 0 0 / 0.5));
}
:host .caption {
grid-area: caption;
grid-area: caption / image;
text-align: center;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 12 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 KiB

View file

@ -9,6 +9,7 @@
<template webc:is="page-head-fonts" webc:nokeep></template>
<template webc:is="page-head-colors" webc:nokeep></template>
<template webc:is="page-head-style" webc:nokeep></template>
<template webc:is="page-head-script" webc:nokeep></template>
<link rel="stylesheet" :href="getBundleFileUrl('css')" webc:keep />
<script type="module" :src="getBundleFileUrl('js')" webc:keep></script>

View file

@ -2,6 +2,11 @@
layout: base.webc
---
<popup-modal id="nsfw-warning" :open="isOpen">
<template webc:nokeep slot="heading">Whoa, Nelly!</template>
<template webc:nokeep slot="message">Here be kinky dragons!</template>
</popup-modal>
<main :class="firstName.toLowerCase()">
<profile>
<eleventy-image

View file

@ -59,6 +59,7 @@ gallery: [
:@width="[1000]"
:@char="$data.firstName.toLowerCase()"
:@dropshadow="true"
:@nsfw="true"
></ref-img>
<colors :@colors="$data.getColors()"></colors>
@ -85,6 +86,7 @@ Sebin has a muscular build that he keeps in shape with regular exercise.
:@width="[1000]"
:@char="$data.firstName.toLowerCase()"
:@dropshadow="true"
:@nsfw="true"
></ref-img>
<quick-info>
@ -147,6 +149,14 @@ An assortment of additional references how Sebin can be drawn.
></ref-img>
</ref-gallery>
<nsfw-barrier>
<div slot="message">
## Danger Zone
Here be kinky dragons 👀💦
</div>
## Penis
<ref-img
@ -164,3 +174,4 @@ An assortment of additional references how Sebin can be drawn.
</quick-info>
Despite his reptilian appearance, Sebin has nipples, a feature of the human side of his family. Furthermore, his external testicles represent another humanized feature. Where relatives of his ilk possess a slit in which the penis lies protectively, Sebin possesses a pouch-like sheath from which the tip of the penis protrudes slightly. The shape of his shaft is predominantly humanoid, but it is surrounded by ridges and has no equivalent of a foreskin. When aroused, the coal-black shaft swells and pushes out of the sheath until fully erect, the sheath wrapping around the root of the shaft like a ring. However, he can also push it out in a flaccid state, e.g. when needing to pass water.
</nsfw-barrier>

View file

@ -26,10 +26,15 @@ Besides snacking, Sebin also likes to eat hearty and savory things. He doesn't d
Sebin rarely says no to a good beer with friends, just as he rarely says no to a bar tour to try new and interesting cocktails.
## test
<nsfw-barrier>
<div slot="message">
<nsfw-barrier></nsfw-barrier>
## But wait! There's more…
Discover Sebin's kinkier side 😏
</div>
## Kinks
<filter-list :@data="$data.kinks"></filter-list>
</nsfw-barrier>