Chotto.app
Login
Sign Up
ES
App Name
kuyawa / toddy
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"> <!-- App icon background: indigo → purple gradient (matches the Toddy title) --> <defs> <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#6366f1"/> <stop offset="1" stop-color="#a855f7"/> </linearGradient> </defs> <!-- Rounded-square app tile --> <rect x="16" y="16" width="480" height="480" rx="108" fill="url(#bg)"/> <!-- Subtle inner highlight --> <rect x="16" y="16" width="480" height="480" rx="108" fill="none" stroke="#ffffff" stroke-opacity="0.18" stroke-width="6"/> <!-- The app's circular checkbox --> <circle cx="212" cy="256" r="104" fill="none" stroke="#ffffff" stroke-width="26"/> <path d="M168 262 l30 32 l58 -64" fill="none" stroke="#ffffff" stroke-width="26" stroke-linecap="round" stroke-linejoin="round"/> <!-- Priority dots: gray (normal), green (high), red (urgent) --> <circle cx="398" cy="150" r="34" fill="#94a3b8"/> <circle cx="398" cy="256" r="34" fill="#22c55e"/> <circle cx="398" cy="362" r="34" fill="#ef4444"/> </svg>
HTML Content
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Toddy</title> <style> /* ================================================================== Design tokens — light & dark themes ================================================================== */ :root { color-scheme: light; --bg: #f1f5f9; --surface: #ffffff; --surface-2: #f8fafc; --text: #0f172a; --muted: #64748b; --border: #e2e8f0; --accent: #6366f1; --accent-hover: #4f46e5; --accent-soft: rgba(99, 102, 241, 0.12); --danger: #ef4444; --danger-soft: rgba(239, 68, 68, 0.1); /* Priority colours */ --prio-normal-bg: #f1f5f9; --prio-normal-dot: #94a3b8; --prio-high-bg: #dcfce7; --prio-high-dot: #22c55e; --prio-urgent-bg: #fee2e2; --prio-urgent-dot: #ef4444; --shadow: 0 10px 30px rgba(2, 6, 23, 0.08); --radius: 14px; } [data-theme="dark"] { color-scheme: dark; --bg: #0f172a; --surface: #1e293b; --surface-2: #172134; --text: #e2e8f0; --muted: #94a3b8; --border: #334155; --accent: #818cf8; --accent-hover: #6366f1; --accent-soft: rgba(129, 140, 248, 0.16); --danger: #f87171; --danger-soft: rgba(248, 113, 113, 0.14); --prio-normal-bg: #1e293b; --prio-normal-dot: #64748b; --prio-high-bg: #14532d; --prio-high-dot: #4ade80; --prio-urgent-bg: #3b1c1c; --prio-urgent-dot: #f87171; --shadow: 0 12px 32px rgba(0, 0, 0, 0.45); } /* No-JS fallback: follow system preference */ @media (prefers-color-scheme: dark) { :root:not([data-theme]) { color-scheme: dark; --bg: #0f172a; --surface: #1e293b; --surface-2: #172134; --text: #e2e8f0; --muted: #94a3b8; --border: #334155; --accent: #818cf8; --accent-hover: #6366f1; --accent-soft: rgba(129, 140, 248, 0.16); --danger: #f87171; --danger-soft: rgba(248, 113, 113, 0.14); --prio-normal-bg: #1e293b; --prio-normal-dot: #64748b; --prio-high-bg: #14532d; --prio-high-dot: #4ade80; --prio-urgent-bg: #3b1c1c; --prio-urgent-dot: #f87171; --shadow: 0 12px 32px rgba(0, 0, 0, 0.45); } } /* ================================================================== Base ================================================================== */ * { margin: 0; padding: 0; box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; justify-content: center; padding: 48px 16px; transition: background-color 0.25s ease, color 0.25s ease; } :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } .app { width: 100%; max-width: 620px; } /* ================================================================== Header + theme toggle ================================================================== */ .app-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } h1 { font-size: 2rem; letter-spacing: -0.5px; background: linear-gradient(135deg, var(--accent), #a855f7); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } .theme-toggle { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--border); background: var(--surface); color: var(--text); display: grid; place-items: center; cursor: pointer; transition: border-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; } .theme-toggle:hover { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); } .theme-toggle:active { transform: scale(0.92); } .theme-toggle .icon-sun { display: none; } [data-theme="dark"] .theme-toggle .icon-sun { display: block; } [data-theme="dark"] .theme-toggle .icon-moon { display: none; } /* ================================================================== Card ================================================================== */ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 20px; } /* ================================================================== Add form ================================================================== */ .add-form { display: flex; gap: 10px; margin-bottom: 18px; flex-wrap: wrap; } .add-form input { flex: 1; min-width: 140px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); color: var(--text); font-size: 1rem; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .add-form input::placeholder { color: var(--muted); } .add-form input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .prio-select-wrap { flex-shrink: 0; } .prio-select { padding: 11px 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); color: var(--text); font-size: 0.9rem; font-family: inherit; cursor: pointer; transition: border-color 0.2s ease; } .prio-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } /* ================================================================== Buttons ================================================================== */ .btn { border: 1px solid var(--border); background: var(--surface-2); color: var(--text); padding: 10px 16px; border-radius: 10px; font-size: 0.95rem; font-weight: 600; font-family: inherit; cursor: pointer; white-space: nowrap; transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.1s ease; } .btn:hover { border-color: var(--accent); color: var(--accent); } .btn:active { transform: scale(0.97); } .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; } .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; } .btn-ghost { background: transparent; } .btn-sm { padding: 7px 12px; font-size: 0.85rem; border-radius: 8px; } /* ================================================================== Filters ================================================================== */ .filters { display: flex; gap: 4px; flex-wrap: wrap; padding-bottom: 14px; margin-bottom: 6px; border-bottom: 1px solid var(--border); } .filters button { background: none; border: none; font-family: inherit; color: var(--muted); font-size: 0.9rem; font-weight: 600; padding: 7px 12px; border-radius: 999px; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; transition: color 0.2s ease, background-color 0.2s ease; } .filters button:hover { color: var(--text); background: var(--accent-soft); } .filters button.active { color: var(--accent); background: var(--accent-soft); } .count { font-size: 0.75rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; line-height: 1.4; } .filters button.active .count { background: var(--accent); border-color: var(--accent); color: #fff; } /* ================================================================== Priority badge ================================================================== */ .prio-badge { width: 28px; height: 28px; border-radius: 50%; border: none; cursor: pointer; flex-shrink: 0; display: grid; place-items: center; background: var(--prio-normal-bg); transition: transform 0.15s ease, box-shadow 0.2s ease; } .prio-badge:hover { transform: scale(1.15); box-shadow: 0 0 0 3px var(--accent-soft); } .prio-badge:active { transform: scale(0.92); } .prio-dot { width: 10px; height: 10px; border-radius: 50%; display: block; background: var(--prio-normal-dot); } .prio-badge.prio-high { background: var(--prio-high-bg); } .prio-badge.prio-high .prio-dot { background: var(--prio-high-dot); } .prio-badge.prio-urgent { background: var(--prio-urgent-bg); } .prio-badge.prio-urgent .prio-dot { background: var(--prio-urgent-dot); } .completed .prio-normal { background: transparent; } .completed .prio-high { background: transparent; } .completed .prio-urgent { background: transparent; } /* ----- Inline priority form ----- */ .prio-form { display: none; gap: 6px; align-items: center; flex-shrink: 0; } .todo-item.prio-editing .prio-form { display: flex; } .todo-item.prio-editing .prio-badge { display: none; } .prio-form-select { padding: 6px 8px; border: 1px solid var(--accent); border-radius: 8px; background: var(--surface-2); color: var(--text); font-size: 0.85rem; font-family: inherit; cursor: pointer; } .prio-form-select:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-soft); } /* ================================================================== Todo list ================================================================== */ .todo-list { list-style: none; } .todo-item { display: flex; align-items: center; gap: 10px; padding: 12px 4px; border-bottom: 1px solid var(--border); animation: fade-in 0.2s ease; } .todo-item:last-child { border-bottom: none; } .todo-main { flex: 1; min-width: 0; display: flex; align-items: center; } .todo-text { font-size: 1rem; word-break: break-word; line-height: 1.4; } .todo-item.completed .todo-text { text-decoration: line-through; color: var(--muted); } /* Custom circular checkbox */ .checkbox { width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--border); background: transparent; cursor: pointer; flex-shrink: 0; display: grid; place-items: center; color: transparent; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease; } .checkbox::after { content: ""; width: 10px; height: 6px; border-left: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(-45deg) translateY(-1px); } .checkbox:hover { border-color: var(--accent); } .checkbox:active { transform: scale(0.9); } .todo-item.completed .checkbox { background: var(--accent); border-color: var(--accent); color: #fff; } /* ================================================================== Inline title editing ================================================================== */ .edit-form { display: none; gap: 8px; align-items: center; flex: 1; min-width: 0; } .todo-item.editing .edit-form { display: flex; } .todo-item.editing .todo-text { display: none; } .edit-input { flex: 1; min-width: 0; padding: 8px 10px; border: 1px solid var(--accent); border-radius: 8px; background: var(--surface-2); color: var(--text); font-size: 0.95rem; font-family: inherit; } .edit-input:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-soft); } /* ================================================================== Icon buttons ================================================================== */ .icon-btn { background: transparent; border: none; color: var(--muted); cursor: pointer; width: 34px; height: 34px; border-radius: 8px; display: grid; place-items: center; flex-shrink: 0; transition: background-color 0.2s ease, color 0.2s ease; } .icon-btn:hover { background: var(--surface-2); color: var(--text); } .delete-btn:hover { background: var(--danger-soft); color: var(--danger); } .todo-item.editing .edit-toggle { display: none; } /* ================================================================== Empty state + footer ================================================================== */ .empty { text-align: center; color: var(--muted); padding: 32px 0 24px; font-size: 0.95rem; } .app-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; } .credit { text-align: center; color: var(--muted); font-size: 0.8rem; margin-top: 16px; } .credit code { background: var(--surface); border: 1px solid var(--border); padding: 1px 6px; border-radius: 6px; font-size: 0.75em; } /* ================================================================== Responsive ================================================================== */ @media (max-width: 520px) { body { padding: 24px 12px; } .card { padding: 16px; } h1 { font-size: 1.6rem; } .add-form { flex-direction: column; } .add-form .btn { width: 100%; } .prio-select-wrap { width: 100%; } .prio-select { width: 100%; } .todo-item { gap: 8px; flex-wrap: wrap; } .app-footer { flex-direction: column; align-items: flex-start; gap: 10px; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; } } @keyframes fade-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } } </style> </head> <body> <main class="app"> <header class="app-header"> <h1>Toddy</h1> <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" title="Toggle theme"> <svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg> <svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> </button> </header> <section class="card"> <!-- Add --> <form id="add-form" class="add-form" autocomplete="off"> <input id="new-title" type="text" placeholder="What needs to be done?" maxlength="200" required aria-label="New todo" /> <div class="prio-select-wrap"> <select id="new-priority" class="prio-select" aria-label="Priority"> <option value="0">Normal</option> <option value="1">High</option> <option value="2">Urgent</option> </select> </div> <button type="submit" class="btn btn-primary">Add</button> </form> <!-- Filters --> <nav class="filters" id="filters" aria-label="Filter todos"> <button type="button" data-filter="all">All <span class="count" id="count-all">0</span></button> <button type="button" data-filter="active">Active <span class="count" id="count-active">0</span></button> <button type="button" data-filter="completed">Completed <span class="count" id="count-completed">0</span></button> </nav> <!-- List --> <ul class="todo-list" id="todo-list"></ul> <div class="empty" id="empty" hidden></div> <!-- Footer --> <footer class="app-footer"> <span id="items-left">0 items left</span> <button type="button" id="clear-completed" class="btn btn-ghost btn-sm" hidden>Clear completed</button> </footer> </section> <p class="credit"><big><code>I ❤️ DeepSeek</code></big> · by Kuyawa</p> </main> <script> (function () { 'use strict'; /* ================================================================ Helpers ================================================================ */ var $ = function (sel) { return document.querySelector(sel); }; function esc(str) { var div = document.createElement('div'); div.textContent = str == null ? '' : String(str); return div.innerHTML; } function uid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0; var v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } /* ================================================================ State & storage ================================================================ */ var STORAGE_KEY = 'toddy.todos'; var THEME_KEY = 'toddy.theme'; var PRIORITY = { 0: 'normal', 1: 'high', 2: 'urgent' }; var state = { todos: [], filter: 'all' }; function load() { try { var raw = localStorage.getItem(STORAGE_KEY); var parsed = raw ? JSON.parse(raw) : []; state.todos = Array.isArray(parsed) ? parsed : []; } catch (e) { state.todos = []; } } function save() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state.todos)); } catch (e) {} } /* ================================================================ Sorting — completed sink to bottom, then by priority (urgent first), then newest first. ================================================================ */ function sortTodos(list) { return list.slice().sort(function (a, b) { if (a.completed !== b.completed) return a.completed ? 1 : -1; var pa = a.priority || 0; var pb = b.priority || 0; if (pa !== pb) return pb - pa; return (b.createdAt || 0) - (a.createdAt || 0); }); } function visibleTodos() { var all = sortTodos(state.todos); if (state.filter === 'active') return all.filter(function (t) { return !t.completed; }); if (state.filter === 'completed') return all.filter(function (t) { return t.completed; }); return all; } function counts() { var total = state.todos.length; var completed = state.todos.filter(function (t) { return t.completed; }).length; return { total: total, remaining: total - completed, completed: completed }; } /* ================================================================ Rendering ================================================================ */ function render() { var list = $('#todo-list'); var empty = $('#empty'); var visible = visibleTodos(); var c = counts(); // Filter buttons document.querySelectorAll('#filters button').forEach(function (btn) { btn.classList.toggle('active', btn.getAttribute('data-filter') === state.filter); }); // Counts $('#count-all').textContent = c.total; $('#count-active').textContent = c.remaining; $('#count-completed').textContent = c.completed; // Items left + clear button $('#items-left').textContent = c.remaining + ' item' + (c.remaining === 1 ? '' : 's') + ' left'; $('#clear-completed').hidden = c.completed === 0; // List list.textContent = ''; visible.forEach(function (todo) { list.appendChild(renderItem(todo)); }); // Empty state if (visible.length === 0) { empty.hidden = false; if (c.total === 0) { empty.textContent = 'Nothing here yet — add your first todo above! 🎉'; } else { empty.textContent = 'No ' + state.filter + ' todos here. 🎈'; } } else { empty.hidden = true; } } function renderItem(todo) { var li = document.createElement('li'); li.className = 'todo-item' + (todo.completed ? ' completed' : ''); li.dataset.id = todo.id; var label = PRIORITY[todo.priority] || 'normal'; // Priority badge var badge = document.createElement('button'); badge.type = 'button'; badge.className = 'prio-badge prio-' + label; badge.setAttribute('aria-label', 'Priority: ' + label + ' — click to change'); badge.title = 'Priority: ' + label + ' — click to change'; badge.innerHTML = '<span class="prio-dot"></span>'; // Inline priority form var prioForm = document.createElement('form'); prioForm.className = 'prio-form'; prioForm.innerHTML = '<select class="prio-form-select" aria-label="Change priority">' + '<option value="0"' + (todo.priority === 0 ? ' selected' : '') + '>Normal</option>' + '<option value="1"' + (todo.priority === 1 ? ' selected' : '') + '>High</option>' + '<option value="2"' + (todo.priority === 2 ? ' selected' : '') + '>Urgent</option>' + '</select>' + '<button type="submit" class="btn btn-primary btn-sm">Set</button>' + '<button type="button" class="btn btn-ghost btn-sm prio-cancel">Cancel</button>'; // Toggle checkbox var toggleForm = document.createElement('form'); toggleForm.className = 'toggle-form'; toggleForm.innerHTML = '<button type="submit" class="checkbox" role="checkbox" aria-checked="' + (todo.completed ? 'true' : 'false') + '" aria-label="' + (todo.completed ? 'Mark as active' : 'Mark as done') + '"></button>'; // Title + inline edit var main = document.createElement('div'); main.className = 'todo-main'; var text = document.createElement('span'); text.className = 'todo-text'; text.textContent = todo.title; var editForm = document.createElement('form'); editForm.className = 'edit-form'; editForm.innerHTML = '<input type="text" class="edit-input" maxlength="200" required aria-label="Edit todo" />' + '<button type="submit" class="btn btn-primary btn-sm">Save</button>' + '<button type="button" class="btn btn-ghost btn-sm edit-cancel">Cancel</button>'; editForm.querySelector('.edit-input').value = todo.title; main.appendChild(text); main.appendChild(editForm); // Edit button var editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.className = 'icon-btn edit-toggle'; editBtn.setAttribute('aria-label', 'Edit todo'); editBtn.title = 'Edit'; editBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>'; // Delete button var delBtn = document.createElement('button'); delBtn.type = 'button'; delBtn.className = 'icon-btn delete-btn'; delBtn.setAttribute('aria-label', 'Delete todo'); delBtn.title = 'Delete'; delBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>'; li.appendChild(badge); li.appendChild(prioForm); li.appendChild(toggleForm); li.appendChild(main); li.appendChild(editBtn); li.appendChild(delBtn); /* ----- Events ----- */ // Toggle complete toggleForm.addEventListener('submit', function (e) { e.preventDefault(); todo.completed = !todo.completed; save(); render(); }); // Edit title editBtn.addEventListener('click', function () { li.classList.add('editing'); var input = editForm.querySelector('.edit-input'); input.focus(); input.setSelectionRange(input.value.length, input.value.length); }); editForm.querySelector('.edit-cancel').addEventListener('click', function () { li.classList.remove('editing'); }); editForm.querySelector('.edit-input').addEventListener('keydown', function (e) { if (e.key === 'Escape') li.classList.remove('editing'); }); editForm.addEventListener('submit', function (e) { e.preventDefault(); var input = editForm.querySelector('.edit-input'); var title = input.value.trim(); if (title) { todo.title = title; save(); } li.classList.remove('editing'); render(); }); // Delete delBtn.addEventListener('click', function () { state.todos = state.todos.filter(function (t) { return t.id !== todo.id; }); save(); render(); }); // Change priority badge.addEventListener('click', function () { li.classList.add('prio-editing'); prioForm.querySelector('.prio-form-select').focus(); }); prioForm.querySelector('.prio-cancel').addEventListener('click', function () { li.classList.remove('prio-editing'); }); prioForm.querySelector('.prio-form-select').addEventListener('keydown', function (e) { if (e.key === 'Escape') li.classList.remove('prio-editing'); }); prioForm.addEventListener('submit', function (e) { e.preventDefault(); todo.priority = parseInt(prioForm.querySelector('.prio-form-select').value, 10) || 0; save(); li.classList.remove('prio-editing'); render(); }); return li; } /* ================================================================ Add ================================================================ */ $('#add-form').addEventListener('submit', function (e) { e.preventDefault(); var title = $('#new-title').value.trim(); if (!title) return; var priority = parseInt($('#new-priority').value, 10) || 0; state.todos.push({ id: uid(), title: title, completed: false, priority: priority, createdAt: Date.now() }); save(); $('#new-title').value = ''; render(); $('#new-title').focus(); }); /* ================================================================ Filters ================================================================ */ document.querySelectorAll('#filters button').forEach(function (btn) { btn.addEventListener('click', function () { state.filter = btn.getAttribute('data-filter'); render(); }); }); /* ================================================================ Clear completed ================================================================ */ $('#clear-completed').addEventListener('click', function () { state.todos = state.todos.filter(function (t) { return !t.completed; }); save(); render(); }); /* ================================================================ Theme ================================================================ */ var root = document.documentElement; var themeToggle = $('#theme-toggle'); function setTheme(theme) { root.setAttribute('data-theme', theme); if (themeToggle) { var next = theme === 'dark' ? 'light' : 'dark'; themeToggle.setAttribute('aria-label', 'Switch to ' + next + ' mode'); themeToggle.title = 'Switch to ' + next + ' mode'; } } (function initTheme() { var saved = null; try { saved = localStorage.getItem(THEME_KEY); } catch (e) {} if (saved === 'dark' || saved === 'light') { setTheme(saved); } else { var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; setTheme(prefersDark ? 'dark' : 'light'); } })(); if (themeToggle) { themeToggle.addEventListener('click', function () { var current = root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; var next = current === 'dark' ? 'light' : 'dark'; try { localStorage.setItem(THEME_KEY, next); } catch (e) {} setTheme(next); }); } /* ================================================================ Boot ================================================================ */ load(); render(); })(); </script> </body> </html>
Cancel