[260120]
- 색상 코드 변수화 - 라이트 모드 추가
This commit is contained in:
71
index.html
71
index.html
@@ -1,26 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<html lang="ko" class="\">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="style.css?v=1.0.1" />
|
||||
<!-- Preconnect and DNS Prefetch -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Orbitron:wght@800&family=Staatliches&family=Tenor+Sans&display=swap">
|
||||
<link rel="preload" href="./font/NovaBySoristudio-Regular.otf" as="font" type="font/otf" crossorigin>
|
||||
<link rel="preload" href="./font/Solaris-3-Script.otf" as="font" type="font/otf" crossorigin>
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="dns-prefetch" href="https://npm.sori.studio">
|
||||
<link rel="dns-prefetch" href="https://git.sori.studio">
|
||||
<title>sori.studio</title>
|
||||
<meta name="description" content="소리스튜디오: 인프라 상태 모니터링 및 개인용 웹 서비스 대시보드입니다.">
|
||||
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<link rel="preload" href="./font/NovaBySoristudio-Regular.otf" as="font" type="font/otf" crossorigin>
|
||||
<link rel="preload" href="./font/Solaris-3-Script.otf" as="font" type="font/otf" crossorigin>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Anton&family=Orbitron:wght@800&family=Staatliches&family=Tenor+Sans&display=swap"
|
||||
media="print" onload="this.media='all'">
|
||||
<link rel="stylesheet" href="style.css?v=1.0.2" media="print" onload="this.media='all'" />
|
||||
|
||||
<noscript>
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Anton&family=Orbitron:wght@800&family=Staatliches&family=Tenor+Sans&display=swap">
|
||||
<link rel="stylesheet" href="style.css?v=1.0.2" />
|
||||
</noscript>
|
||||
|
||||
<link rel="dns-prefetch" href="https://npm.sori.studio">
|
||||
<link rel="dns-prefetch" href="https://git.sori.studio">
|
||||
</head>
|
||||
|
||||
<body class="semi-nova m-0 p-0 flex flex-column align-center keep-all">
|
||||
<header class="flex align-center h-20 pt-20 px-4 pb-12 text-center">
|
||||
<h1 class="header-title m-0 p-0">sori<span class="dot-point">.</span>studio</h1>
|
||||
<div class="theme-toggle-wrapper">
|
||||
<button class="theme-toggle-btn" aria-label="테마 변경">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M12 2 A10 10 0 0 1 12 22" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="theme-options">
|
||||
<button class="theme-option" data-theme="dark">
|
||||
<span class="theme-option-icon">●</span>
|
||||
<span class="theme-option-label">Dark</span>
|
||||
</button>
|
||||
<button class="theme-option" data-theme="light">
|
||||
<span class="theme-option-icon">○</span>
|
||||
<span class="theme-option-label">Light</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@@ -32,7 +61,9 @@
|
||||
<!-- Nginx Proxy Manager -->
|
||||
<a href="https://npm.sori.studio" class="card" data-url="https://npm.sori.studio">
|
||||
<!-- Decorative Element -->
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span
|
||||
class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span
|
||||
class="card__decor card__decor--br"></span></div>
|
||||
|
||||
<!-- Card Title -->
|
||||
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
|
||||
@@ -51,7 +82,9 @@
|
||||
<!-- gitea -->
|
||||
<a href="https://git.sori.studio" class="card" data-url="https://git.sori.studio">
|
||||
<!-- Decorative Element -->
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span
|
||||
class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span
|
||||
class="card__decor card__decor--br"></span></div>
|
||||
|
||||
<!-- Card Title -->
|
||||
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
|
||||
@@ -70,7 +103,9 @@
|
||||
<!-- Pocket Base -->
|
||||
<a href="https://api.sori.studio" class="card" data-url="https://api.sori.studio">
|
||||
<!-- Decorative Element -->
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span
|
||||
class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span
|
||||
class="card__decor card__decor--br"></span></div>
|
||||
|
||||
<!-- Card Title -->
|
||||
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
|
||||
@@ -97,7 +132,9 @@
|
||||
<!-- union arena deck builder -->
|
||||
<a href="https://uniare.sori.studio" class="card" data-url="https://uniare.sori.studio">
|
||||
<!-- Decorative Element -->
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span
|
||||
class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span
|
||||
class="card__decor card__decor--br"></span></div>
|
||||
|
||||
<!-- Card Title -->
|
||||
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
|
||||
@@ -124,7 +161,9 @@
|
||||
<!-- mastodon -->
|
||||
<a href="https://sns.sori.studio" class="card" data-url="https://sns.sori.studio">
|
||||
<!-- Decorative Element -->
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
|
||||
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span
|
||||
class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span
|
||||
class="card__decor card__decor--br"></span></div>
|
||||
|
||||
<!-- Card Title -->
|
||||
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
|
||||
|
||||
153
script.js
153
script.js
@@ -1,59 +1,42 @@
|
||||
// Focus dot animation
|
||||
const dot = document.querySelector('.dot-point');
|
||||
|
||||
function updateDotState() {
|
||||
function updateDotState() {
|
||||
const isActive =
|
||||
document.visibilityState === 'visible' &&
|
||||
document.hasFocus();
|
||||
|
||||
dot.classList.toggle('is-active', isActive);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', updateDotState);
|
||||
window.addEventListener('focus', updateDotState);
|
||||
window.addEventListener('blur', updateDotState);
|
||||
document.addEventListener('visibilitychange', updateDotState);
|
||||
window.addEventListener('focus', updateDotState);
|
||||
window.addEventListener('blur', updateDotState);
|
||||
|
||||
// 최초 실행
|
||||
updateDotState();
|
||||
updateDotState();
|
||||
|
||||
// ping
|
||||
async function checkStatus() {
|
||||
const cards = document.querySelectorAll(".card");
|
||||
const results = [];
|
||||
|
||||
for (const card of cards) {
|
||||
const checkTasks = Array.from(cards).map(async (card) => {
|
||||
const url = card.getAttribute("data-url");
|
||||
const dot = card.querySelector(".card__decor.card__decor--tr");
|
||||
|
||||
// 결과 저장을 위한 약속(Promise) 생성
|
||||
const checkTask = new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
// 5초 타임아웃 설정
|
||||
const timeoutId = setTimeout(() => {
|
||||
img.src = "";
|
||||
resolve({ dot, status: "offline" });
|
||||
}, 5000);
|
||||
|
||||
img.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve({ dot, status: "online" });
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve({ dot, status: "offline" }); // 502 에러 시 이쪽으로 빠집니다.
|
||||
};
|
||||
|
||||
// 캐시 방지를 위해 타임스탬프를 붙여서 파비콘 호출
|
||||
img.src = `${url}/favicon.ico?t=${new Date().getTime()}`;
|
||||
try {
|
||||
// mode: 'no-cors'를 사용하면 타 도메인의 리소스 존재 여부만 빠르게 확인 가능
|
||||
const response = await fetch(`${url}/favicon.ico?t=${Date.now()}`, {
|
||||
mode: 'no-cors',
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
results.push(checkTask);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100)); // 0.1초 간격 요청
|
||||
return { dot, status: "online" };
|
||||
} catch (error) {
|
||||
return { dot, status: "offline" };
|
||||
}
|
||||
});
|
||||
|
||||
// 모든 결과가 모이면 한꺼번에 반영
|
||||
const finalStatuses = await Promise.all(results);
|
||||
const finalStatuses = await Promise.all(checkTasks);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
finalStatuses.forEach(({ dot, status }) => {
|
||||
@@ -66,9 +49,8 @@ async function checkStatus() {
|
||||
checkStatus();
|
||||
setInterval(checkStatus, 300000);
|
||||
|
||||
// 🔴 테스트 ON
|
||||
// bolt animation - Test Mode
|
||||
// const TEST_ONLY_SECOND_CARD = true;
|
||||
// 🟢 테스트 OFF
|
||||
const TEST_ONLY_SECOND_CARD = false;
|
||||
|
||||
// bolt animation
|
||||
@@ -101,10 +83,9 @@ function startBoltAction() {
|
||||
|
||||
randomBolt.classList.add("is-acting");
|
||||
|
||||
// setTimeout 대신 이벤트 리스너 사용
|
||||
randomBolt.addEventListener('animationend', () => {
|
||||
randomBolt.classList.remove("is-acting");
|
||||
}, { once: true }); // 메모리 관리를 위해 한 번만 실행 후 제거
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
const nextInterval = Math.random() * 4000 + 3000;
|
||||
@@ -116,10 +97,8 @@ window.addEventListener("load", () => {
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. 상태 체크는 1초 뒤에 여유롭게 시작
|
||||
setTimeout(checkStatus, 1000);
|
||||
|
||||
// 2. 나사 랜덤 각도 설정 (DOM이 준비된 직후 실행)
|
||||
document.querySelectorAll('.card').forEach((card) => {
|
||||
card.querySelectorAll('.card__decor').forEach((decor) => {
|
||||
const angle = Math.floor(Math.random() * 180) - 30;
|
||||
@@ -128,48 +107,39 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 1. 사용할 테마 리스트
|
||||
// Text Changer
|
||||
const themes = ['semi-nova', 'nova', 'semi-solaris', 'solaris'];
|
||||
const STORAGE_KEY = 'selected-theme';
|
||||
|
||||
// 2. 초기 로드 시 테마 적용 (기본값: semi-nova)
|
||||
const savedTheme = localStorage.getItem(STORAGE_KEY) || themes[0];
|
||||
document.body.classList.add(savedTheme);
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// OS별 수정 키 판별 (Mac: Command, Win: Control)
|
||||
const isMac = navigator.platform.toUpperCase().includes('MAC');
|
||||
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
// 방향키 좌/우 확인
|
||||
const isLeft = e.code === 'ArrowLeft';
|
||||
const isRight = e.code === 'ArrowRight';
|
||||
|
||||
if (
|
||||
isModifierPressed &&
|
||||
(isLeft || isRight) &&
|
||||
// 입력창 안에서 커서 이동을 방해하지 않도록 체크
|
||||
!['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) &&
|
||||
!document.activeElement?.isContentEditable // 에디터 영역 대응
|
||||
!document.activeElement?.isContentEditable
|
||||
) {
|
||||
e.preventDefault();
|
||||
|
||||
// 현재 인덱스 찾기
|
||||
let currentIndex = themes.findIndex(t => document.body.classList.contains(t));
|
||||
if (currentIndex === -1) currentIndex = 0;
|
||||
|
||||
// 3. 좌/우 방향에 따른 인덱스 순환
|
||||
if (isRight) {
|
||||
// 오른쪽 화살표: 다음 테마
|
||||
currentIndex = (currentIndex + 1) % themes.length;
|
||||
} else if (isLeft) {
|
||||
// 왼쪽 화살표: 이전 테마
|
||||
currentIndex = (currentIndex - 1 + themes.length) % themes.length;
|
||||
}
|
||||
|
||||
const nextTheme = themes[currentIndex];
|
||||
|
||||
// 4. 클래스 교체 및 저장
|
||||
themes.forEach(t => document.body.classList.remove(t));
|
||||
document.body.classList.add(nextTheme);
|
||||
localStorage.setItem(STORAGE_KEY, nextTheme);
|
||||
@@ -178,6 +148,7 @@ window.addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Text Changer - Swipe Gesture
|
||||
const logo = document.querySelector('header h1');
|
||||
|
||||
let startX = 0;
|
||||
@@ -196,8 +167,6 @@ function applyTheme(index) {
|
||||
const dot = document.querySelector('.dot-point');
|
||||
if (dot) {
|
||||
dot.classList.remove('blink-alert');
|
||||
// 리플로우 유발 코드 제거 (void dot.offsetWidth;)
|
||||
// 대신 브라우저의 다음 렌더링 사이클을 이용
|
||||
requestAnimationFrame(() => {
|
||||
dot.classList.add('blink-alert');
|
||||
});
|
||||
@@ -238,3 +207,81 @@ function reset() {
|
||||
isDragging = false;
|
||||
logo.classList.remove('is-sliding');
|
||||
}
|
||||
|
||||
// Theme Toggle Button
|
||||
const THEME_STORAGE_KEY = 'color-theme';
|
||||
const themeWrapper = document.querySelector('.theme-toggle-wrapper');
|
||||
const themeToggleBtn = document.querySelector('.theme-toggle-btn');
|
||||
const themeOptions = document.querySelectorAll('.theme-option');
|
||||
|
||||
function initTheme() {
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) || 'dark';
|
||||
applyColorTheme(savedTheme);
|
||||
updateActiveState(savedTheme);
|
||||
}
|
||||
|
||||
function applyColorTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.add('light-mode');
|
||||
} else {
|
||||
document.documentElement.classList.remove('light-mode');
|
||||
}
|
||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
}
|
||||
|
||||
function updateActiveState(theme) {
|
||||
themeOptions.forEach(option => {
|
||||
if (option.dataset.theme === theme) {
|
||||
option.classList.add('is-active');
|
||||
} else {
|
||||
option.classList.remove('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
themeToggleBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
themeWrapper.classList.toggle('is-open');
|
||||
});
|
||||
|
||||
themeOptions.forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const selectedTheme = option.dataset.theme;
|
||||
applyColorTheme(selectedTheme);
|
||||
updateActiveState(selectedTheme);
|
||||
themeWrapper.classList.remove('is-open');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
themeWrapper.classList.remove('is-open');
|
||||
});
|
||||
|
||||
initTheme();
|
||||
|
||||
// Theme toggle - Keyboard Shortcut
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const isMac = navigator.platform.toUpperCase().includes('MAC');
|
||||
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
const isUp = e.code === 'ArrowUp';
|
||||
const isDown = e.code === 'ArrowDown';
|
||||
|
||||
if (
|
||||
isModifierPressed &&
|
||||
(isUp || isDown) &&
|
||||
!['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) &&
|
||||
!document.activeElement?.isContentEditable
|
||||
) {
|
||||
e.preventDefault();
|
||||
|
||||
const currentTheme = localStorage.getItem(THEME_STORAGE_KEY) || 'dark';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
applyColorTheme(newTheme);
|
||||
updateActiveState(newTheme);
|
||||
|
||||
console.log(`Color Theme: ${newTheme}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user