442 lines
14 KiB
HTML
442 lines
14 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="description" content="Simple homepage for self-hosted programs.">
|
|
<title>teto.dance</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Sniglet:wght@400;800&family=DynaPuff:wght@400;700&display=swap" rel="stylesheet">
|
|
<link rel="icon" type="image/svg+xml" href="images/favicon.svg">
|
|
<style>
|
|
:root {
|
|
--text-sm: clamp(0.875rem, 0.82rem + 0.25vw, 1rem);
|
|
--text-base: clamp(1rem, 0.96rem + 0.25vw, 1.125rem);
|
|
--text-lg: clamp(1.125rem, 1rem + 0.75vw, 1.5rem);
|
|
--text-xl: clamp(1.65rem, 1.1rem + 2vw, 2.8rem);
|
|
|
|
--space-2: 0.5rem;
|
|
--space-3: 0.75rem;
|
|
--space-4: 1rem;
|
|
--space-6: 1.5rem;
|
|
--space-8: 2rem;
|
|
--space-10: 2.5rem;
|
|
--space-12: 3rem;
|
|
|
|
--radius-full: 9999px;
|
|
--radius-lg: 1rem;
|
|
--radius-xl: 1.5rem;
|
|
|
|
--font-body: 'Sniglet', system-ui, sans-serif;
|
|
--font-heading: 'DynaPuff', system-ui, sans-serif;
|
|
--bg-image: url("images/wallpaper.png");
|
|
--toggle-size: 2.8rem;
|
|
}
|
|
|
|
:root,
|
|
[data-theme="light"] {
|
|
--color-bg: #eef3fb;
|
|
--color-surface: rgba(255, 255, 255, 0.66);
|
|
--color-border: rgba(28, 37, 54, 0.12);
|
|
--color-text: #162033;
|
|
--color-text-muted: rgba(22, 32, 51, 0.72);
|
|
--color-primary: #316dff;
|
|
--color-heading-subtle: #52627e;
|
|
--color-overlay: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(233, 240, 250, 0.56));
|
|
--image-filter: blur(7px) brightness(0.84);
|
|
--shadow-lg: 0 20px 60px rgba(30, 51, 84, 0.18);
|
|
--toggle-bg: rgba(255, 255, 255, 0.74);
|
|
--toggle-border: rgba(28, 37, 54, 0.12);
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--color-bg: #0d1016;
|
|
--color-surface: rgba(17, 19, 26, 0.46);
|
|
--color-border: rgba(255, 255, 255, 0.14);
|
|
--color-text: #f5f7fb;
|
|
--color-text-muted: rgba(245, 247, 251, 0.72);
|
|
--color-primary: #9dd6ff;
|
|
--color-heading-subtle: #bababa;
|
|
--color-overlay: linear-gradient(180deg, rgba(8, 10, 16, 0.3), rgba(8, 10, 16, 0.6));
|
|
--image-filter: blur(7px) brightness(0.42);
|
|
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.35);
|
|
--toggle-bg: rgba(17, 19, 26, 0.68);
|
|
--toggle-border: rgba(255, 255, 255, 0.12);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme]) {
|
|
--color-bg: #0d1016;
|
|
--color-surface: rgba(17, 19, 26, 0.46);
|
|
--color-border: rgba(255, 255, 255, 0.14);
|
|
--color-text: #f5f7fb;
|
|
--color-text-muted: rgba(245, 247, 251, 0.72);
|
|
--color-primary: #9dd6ff;
|
|
--color-heading-subtle: #bababa;
|
|
--color-overlay: linear-gradient(180deg, rgba(8, 10, 16, 0.3), rgba(8, 10, 16, 0.6));
|
|
--image-filter: blur(7px) brightness(0.42);
|
|
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.35);
|
|
--toggle-bg: rgba(17, 19, 26, 0.68);
|
|
--toggle-border: rgba(255, 255, 255, 0.12);
|
|
}
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; min-height: 100%; }
|
|
html { background: var(--color-bg); }
|
|
body {
|
|
font-family: var(--font-body);
|
|
font-size: var(--text-base);
|
|
color: var(--color-text);
|
|
background: var(--color-bg);
|
|
overflow: hidden;
|
|
position: relative;
|
|
transition: background 220ms ease, color 220ms ease;
|
|
}
|
|
|
|
body::before {
|
|
content: "";
|
|
position: fixed;
|
|
inset: -2rem;
|
|
background-image: var(--bg-image);
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
filter: var(--image-filter);
|
|
transform: scale(1.06);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
transition: filter 220ms ease;
|
|
}
|
|
|
|
body::after {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--color-overlay);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
transition: background 220ms ease;
|
|
}
|
|
|
|
.page {
|
|
position: relative;
|
|
min-height: 100dvh;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: var(--space-8);
|
|
isolation: isolate;
|
|
}
|
|
|
|
.theme-toggle {
|
|
position: fixed;
|
|
top: var(--space-6);
|
|
right: var(--space-6);
|
|
width: var(--toggle-size);
|
|
height: var(--toggle-size);
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: var(--radius-full);
|
|
border: 1px solid var(--toggle-border);
|
|
background: var(--toggle-bg);
|
|
color: var(--color-text);
|
|
backdrop-filter: blur(14px);
|
|
box-shadow: var(--shadow-lg);
|
|
cursor: pointer;
|
|
z-index: 5;
|
|
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), background 220ms ease, border-color 220ms ease;
|
|
}
|
|
|
|
.theme-toggle:hover,
|
|
.theme-toggle:focus-visible {
|
|
transform: translateY(-2px);
|
|
outline: none;
|
|
}
|
|
|
|
.theme-toggle svg {
|
|
width: 1.2rem;
|
|
height: 1.2rem;
|
|
stroke: currentColor;
|
|
fill: none;
|
|
stroke-width: 1.8;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.panel {
|
|
width: min(100%, 34rem);
|
|
padding: clamp(var(--space-8), 6vw, var(--space-12));
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-xl);
|
|
background: var(--color-surface);
|
|
box-shadow: var(--shadow-lg);
|
|
backdrop-filter: blur(18px);
|
|
text-align: center;
|
|
transition: background 220ms ease, border-color 220ms ease, box-shadow 220ms ease;
|
|
}
|
|
|
|
.avatar {
|
|
width: clamp(7rem, 16vw, 9.5rem);
|
|
aspect-ratio: 1;
|
|
margin: 0 auto var(--space-6);
|
|
border-radius: var(--radius-full);
|
|
padding: 0.35rem;
|
|
background: #ffffff;
|
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
touch-action: manipulation;
|
|
will-change: transform;
|
|
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.avatar-inner {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: var(--radius-full);
|
|
display: block;
|
|
object-fit: cover;
|
|
border: 1px solid rgba(255,255,255,0.16);
|
|
pointer-events: none;
|
|
will-change: transform;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0 0 var(--space-2);
|
|
font: var(--font-heading);
|
|
font-size: var(--text-xl);
|
|
line-height: 1.1;
|
|
}
|
|
|
|
h2 {
|
|
margin: 0;
|
|
font-size: var(--text-lg);
|
|
line-height: 1.2;
|
|
font-weight: 300;
|
|
}
|
|
|
|
h4 {
|
|
margin: 0 0 var(--space-6);
|
|
color: var(--color-heading-subtle);
|
|
font-weight: 500;
|
|
transition: color 220ms ease;
|
|
}
|
|
|
|
.links {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: var(--space-6) 0 0;
|
|
display: grid;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.links li {
|
|
opacity: 0;
|
|
transform: translateY(16px) scale(0.98);
|
|
animation: slideIn 700ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
}
|
|
|
|
.links li:nth-child(2) { animation-delay: 120ms; }
|
|
.links li:nth-child(3) { animation-delay: 220ms; }
|
|
|
|
.link-card {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--space-4);
|
|
padding: var(--space-4) var(--space-6);
|
|
border-radius: var(--radius-lg);
|
|
color: var(--color-text);
|
|
text-decoration: none;
|
|
text-align: center;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
background: rgba(255,255,255,0.06);
|
|
backdrop-filter: blur(10px);
|
|
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), background 180ms ease, border-color 180ms ease, color 220ms ease;
|
|
min-height: 3.5rem;
|
|
}
|
|
|
|
[data-theme="light"] .link-card,
|
|
:root:not([data-theme]) .link-card {
|
|
border-color: rgba(28, 37, 54, 0.08);
|
|
background: rgba(255, 255, 255, 0.46);
|
|
}
|
|
|
|
.link-card:hover,
|
|
.link-card:focus-visible {
|
|
transform: translateY(-2px);
|
|
background: rgba(255,255,255,0.1);
|
|
border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
|
|
outline: none;
|
|
}
|
|
|
|
.link-name {
|
|
font-weight: 300;
|
|
letter-spacing: 0.01em;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.credit {
|
|
margin: var(--space-8) 0 0;
|
|
color: var(--color-heading-subtle);
|
|
font-size: var(--text-sm);
|
|
transition: color 220ms ease;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
body { overflow: auto; }
|
|
.page { padding: var(--space-4); }
|
|
.panel { padding: var(--space-8) var(--space-6); }
|
|
.link-card { padding: var(--space-4); }
|
|
.theme-toggle {
|
|
top: var(--space-4);
|
|
right: var(--space-4);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*, *::before, *::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
.links li { opacity: 1; transform: none; }
|
|
.avatar, .avatar-inner { transform: none !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" type="button" aria-label="Toggle color mode" data-theme-toggle>
|
|
<span class="theme-toggle-icon" aria-hidden="true"></span>
|
|
</button>
|
|
|
|
<main class="page">
|
|
<section class="panel" aria-label="Self-hosted homepage">
|
|
<h1>Teto.Dance</h1>
|
|
<h4>Little Apps For Little Things</h4>
|
|
<div class="avatar" aria-label="Spinning Teto profile image" tabindex="0">
|
|
<img class="avatar-inner" src="images/pfp.jpg" alt="Dinosaur">
|
|
</div>
|
|
<h2>Applications</h2>
|
|
<ul class="links" role="list">
|
|
<li>
|
|
<a class="link-card" href="https://music.teto.dance" target="_blank" rel="noopener noreferrer">
|
|
<span class="link-name">Navidrome | 📻</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="link-card" href="https://image.teto.dance" target="_blank" rel="noopener noreferrer">
|
|
<span class="link-name">Immich | 🖼️</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="link-card" href="https://code.teto.dance" target="_blank" rel="noopener noreferrer">
|
|
<span class="link-name">Forgejo | 🧰</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="link-card" href="https://vault.teto.dance" target="_blank" rel="noopener noreferrer">
|
|
<span class="link-name">Vaultwarden | 🔒</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
<p class="credit">♡ Made By WeeScottishMan ♡</p>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
const avatar = document.querySelector('.avatar');
|
|
const avatarInner = document.querySelector('.avatar-inner');
|
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
const themeToggle = document.querySelector('[data-theme-toggle]');
|
|
const themeIcon = document.querySelector('.theme-toggle-icon');
|
|
const root = document.documentElement;
|
|
const schemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
let manualTheme = null;
|
|
|
|
const sunIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.5M12 19.5V22M4.93 4.93l1.77 1.77M17.3 17.3l1.77 1.77M2 12h2.5M19.5 12H22M4.93 19.07l1.77-1.77M17.3 6.7l1.77-1.77"></path></svg>';
|
|
const moonIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12.4A8.5 8.5 0 1 1 11.6 3a7 7 0 0 0 9.4 9.4Z"></path></svg>';
|
|
|
|
const applyTheme = (theme, manual = false) => {
|
|
if (manual) {
|
|
manualTheme = theme;
|
|
root.setAttribute('data-theme', theme);
|
|
} else if (manualTheme) {
|
|
root.setAttribute('data-theme', manualTheme);
|
|
} else {
|
|
root.setAttribute('data-theme', theme);
|
|
}
|
|
|
|
const activeTheme = root.getAttribute('data-theme');
|
|
themeIcon.innerHTML = activeTheme === 'dark' ? sunIcon : moonIcon;
|
|
themeToggle.setAttribute('aria-label', activeTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
|
|
};
|
|
|
|
applyTheme(schemeQuery.matches ? 'dark' : 'light');
|
|
|
|
schemeQuery.addEventListener('change', (event) => {
|
|
if (!manualTheme) {
|
|
applyTheme(event.matches ? 'dark' : 'light');
|
|
}
|
|
});
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const current = root.getAttribute('data-theme') || (schemeQuery.matches ? 'dark' : 'light');
|
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
applyTheme(next, true);
|
|
});
|
|
|
|
if (avatar && avatarInner && !reduceMotion.matches) {
|
|
let angle = 0;
|
|
let velocity = 0.18;
|
|
let rafId = null;
|
|
let lastTime = 0;
|
|
|
|
const maxVelocity = 1000;
|
|
const minVelocity = 0.12;
|
|
const boostAmount = 0.16;
|
|
const decay = 0.996;
|
|
|
|
const animate = (time) => {
|
|
if (!lastTime) lastTime = time;
|
|
const delta = Math.min(32, time - lastTime);
|
|
lastTime = time;
|
|
|
|
velocity *= Math.pow(decay, delta / 16.67);
|
|
velocity = Math.max(minVelocity, Math.min(maxVelocity, velocity));
|
|
|
|
angle += velocity * delta;
|
|
avatar.style.transform = `scale(${1 + Math.min((velocity - minVelocity) * 0.08, 0.08)})`;
|
|
avatarInner.style.transform = `rotate(${angle}deg)`;
|
|
|
|
rafId = requestAnimationFrame(animate);
|
|
};
|
|
|
|
const addBoost = () => {
|
|
velocity = Math.min(maxVelocity, velocity + boostAmount);
|
|
if (!rafId) rafId = requestAnimationFrame(animate);
|
|
};
|
|
|
|
avatar.addEventListener('mouseenter', addBoost);
|
|
avatar.addEventListener('mouseleave', addBoost);
|
|
avatar.addEventListener('focus', addBoost);
|
|
avatar.addEventListener('blur', addBoost);
|
|
avatar.addEventListener('touchstart', addBoost, { passive: true });
|
|
|
|
rafId = requestAnimationFrame(animate);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
EOF && grep -n "DynaPuff\|data-theme-toggle\|prefers-color-scheme" output/simple-homepage/selfhosted-homepage.html | head -n 20
|