A premium glowing userscript dashboard featuring persistent counters, a task manager, and a stopwatch/timer with drag-and-resize capabilities.
// ==UserScript==
// @name Ultimate Glow Dashboard
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description A premium glowing userscript dashboard featuring persistent counters, a task manager, and a stopwatch/timer with drag-and-resize capabilities.
// @author ksmo
// @match *://*/*
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'ksmo_glow_dashboard_state';
let state = {
visible: false,
posX: window.innerWidth - 300,
posY: 100,
width: 260,
height: 380,
locked: false,
activeTab: 'counters',
counters: [
{ id: '1', name: 'User 1', value: 0, color: '#ff6bff' },
{ id: '2', name: 'User 2', value: 0, color: '#ffb834' }
],
tasks: [
{ id: '1', text: 'Task 1', completed: false }
],
pinY: window.innerHeight / 2 - 22,
timer: { seconds: 0, running: false, startedAt: null }
};
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed && typeof parsed === 'object') {
if (Array.isArray(parsed.counters)) state.counters = parsed.counters;
if (Array.isArray(parsed.tasks)) state.tasks = parsed.tasks;
if (typeof parsed.visible === 'boolean') state.visible = parsed.visible;
if (typeof parsed.locked === 'boolean') state.locked = parsed.locked;
if (typeof parsed.activeTab === 'string') state.activeTab = parsed.activeTab;
if (typeof parsed.posX === 'number') state.posX = parsed.posX;
if (typeof parsed.posY === 'number') state.posY = parsed.posY;
if (typeof parsed.width === 'number') state.width = parsed.width;
if (typeof parsed.height === 'number') state.height = parsed.height;
if (typeof parsed.pinY === 'number') state.pinY = parsed.pinY;
if (parsed.timer && typeof parsed.timer === 'object') {
state.timer = parsed.timer;
}
}
}
} catch (e) {
console.error('Failed to load state:', e);
}
const saveState = () => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (e) {
console.error('Failed to save state:', e);
}
};
const generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
if (!state.timer) {
state.timer = { seconds: 0, running: false, startedAt: null };
}
let timerInterval = null;
let timerSeconds = state.timer.seconds || 0;
let isTimerRunning = state.timer.running || false;
let timerDisplayEl = null;
const formatTime = (totalSeconds) => {
const hrs = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
return [
hrs > 0 ? String(hrs).padStart(2, '0') : null,
String(mins).padStart(2, '0'),
String(secs).padStart(2, '0')
].filter(Boolean).join(':');
};
const updateTimerGlobal = () => {
timerSeconds++;
if (timerDisplayEl) {
timerDisplayEl.textContent = formatTime(timerSeconds);
}
};
const startTimer = () => {
clearInterval(timerInterval);
clearTimeout(timerInterval);
if (isTimerRunning && state.timer.startedAt) {
const elapsedMs = Date.now() - state.timer.startedAt;
timerSeconds = state.timer.seconds + Math.floor(elapsedMs / 1000);
if (timerDisplayEl) {
timerDisplayEl.textContent = formatTime(timerSeconds);
}
const msToNextSecond = 1000 - (elapsedMs % 1000);
timerInterval = setTimeout(() => {
updateTimerGlobal();
timerInterval = setInterval(updateTimerGlobal, 1000);
}, msToNextSecond);
}
};
if (isTimerRunning) {
startTimer();
}
const clampPos = (x, y, w, h) => {
return {
x: Math.max(0, Math.min(window.innerWidth - w, x)),
y: Math.max(0, Math.min(window.innerHeight - h, y))
};
};
const pos = clampPos(state.posX, state.posY, state.width, state.height);
state.posX = pos.x;
state.posY = pos.y;
const host = document.createElement('div');
host.id = 'ksmo-glow-dash-host';
host.style.position = 'fixed';
host.style.top = '0';
host.style.left = '0';
host.style.width = '0';
host.style.height = '0';
host.style.zIndex = '999999';
const shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
.ksmo-panel {
position: fixed;
background: radial-gradient(circle at top left, rgba(25, 25, 40, 0.9), rgba(12, 12, 20, 0.96));
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.7);
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
box-sizing: border-box;
user-select: none;
overflow: hidden;
opacity: 0;
transform: scale(0.92) translateY(10px);
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none;
}
.ksmo-panel.show {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.ksmo-header {
padding: 0.8em 1.2em;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
cursor: grab;
flex-shrink: 0;
}
.ksmo-header.locked {
cursor: default;
}
.ksmo-header:active:not(.locked) {
cursor: grabbing;
}
.ksmo-title {
font-size: 0.85em;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: rgba(255, 255, 255, 0.5);
}
.ksmo-header-actions {
display: flex;
gap: 0.5em;
}
.ksmo-icon-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
font-size: 1.1em;
padding: 0.2em 0.5em;
border-radius: 6px;
transition: all 0.2s ease;
opacity: 0.5;
display: flex;
align-items: center;
justify-content: center;
}
.ksmo-icon-btn:hover {
background: rgba(255, 255, 255, 0.08);
opacity: 1;
transform: scale(1.05);
}
.ksmo-icon-btn:active {
transform: scale(0.95);
}
.ksmo-tabs {
display: flex;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
background: rgba(0, 0, 0, 0.15);
flex-shrink: 0;
}
.ksmo-tab {
flex: 1;
padding: 0.8em 0;
background: none;
border: none;
color: rgba(255, 255, 255, 0.4);
font-size: 0.75em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
border-bottom: 2px solid transparent;
text-align: center;
}
.ksmo-tab:hover {
color: rgba(255, 255, 255, 0.8);
}
.ksmo-tab.active {
color: #ffffff;
border-bottom-color: #6c5ce7;
background: rgba(255, 255, 255, 0.02);
text-shadow: 0 0 12px rgba(108, 92, 231, 0.6);
}
.ksmo-content {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 1.2em;
overflow-y: auto;
position: relative;
gap: 1em;
}
.ksmo-view {
display: none;
flex-direction: column;
height: 100%;
}
.ksmo-view.active {
display: flex;
}
.ksmo-counter-list {
display: flex;
flex-direction: column;
gap: 0.8em;
flex-grow: 1;
overflow-y: auto;
}
.ksmo-counter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.8em 1em;
border-radius: 10px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
transition: all 0.2s ease;
}
.ksmo-counter-item:hover {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.06);
}
.ksmo-counter-info {
display: flex;
flex-direction: column;
gap: 0.2em;
cursor: pointer;
max-width: 50%;
}
.ksmo-counter-name {
font-size: 0.9em;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.2s;
}
.ksmo-counter-val {
font-size: 1.8em;
font-weight: 700;
line-height: 1.1;
transition: text-shadow 0.3s ease;
}
.ksmo-counter-actions {
display: flex;
align-items: center;
gap: 0.4em;
}
.ksmo-btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.04);
color: #fff;
font-size: 1.1em;
font-weight: 600;
width: 2.2em;
height: 2.2em;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.ksmo-btn:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
.ksmo-btn:active {
transform: translateY(0) scale(0.95);
background: rgba(255, 255, 255, 0.03);
}
.ksmo-btn.danger {
background: rgba(255, 76, 76, 0.1);
border-color: rgba(255, 76, 76, 0.15);
color: #ff4c4c;
}
.ksmo-btn.danger:hover {
background: rgba(255, 76, 76, 0.25);
border-color: rgba(255, 76, 76, 0.3);
}
.ksmo-add-btn {
margin-top: 0.8em;
width: 100%;
padding: 0.8em 0;
background: rgba(108, 92, 231, 0.15);
border: 1px solid rgba(108, 92, 231, 0.25);
color: #a29bfe;
border-radius: 10px;
font-size: 0.85em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.ksmo-add-btn:hover {
background: rgba(108, 92, 231, 0.3);
color: #fff;
box-shadow: 0 4px 15px rgba(108, 92, 231, 0.2);
}
.ksmo-task-input-row {
display: flex;
gap: 0.5em;
margin-bottom: 0.8em;
flex-shrink: 0;
}
.ksmo-input {
flex: 1;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 0.6em 0.9em;
color: #fff;
font-size: 0.9em;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.ksmo-input:focus {
border-color: rgba(108, 92, 231, 0.6);
box-shadow: 0 0 10px rgba(108, 92, 231, 0.2);
}
.ksmo-task-list {
display: flex;
flex-direction: column;
gap: 0.6em;
flex-grow: 1;
overflow-y: auto;
}
.ksmo-task-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6em 0.8em;
background: rgba(255, 255, 255, 0.01);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.03);
transition: all 0.2s;
}
.ksmo-task-item:hover {
background: rgba(255, 255, 255, 0.03);
}
.ksmo-task-checkbox {
cursor: pointer;
margin-right: 0.8em;
width: 1.1em;
height: 1.1em;
accent-color: #6c5ce7;
}
.ksmo-task-text {
flex-grow: 1;
font-size: 0.85em;
word-break: break-word;
transition: opacity 0.2s, text-decoration 0.2s;
}
.ksmo-task-text.completed {
text-decoration: line-through;
opacity: 0.35;
}
.ksmo-timer-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
gap: 1.5em;
}
.ksmo-timer-display {
font-size: 2.8em;
font-weight: 700;
font-family: monospace;
color: #00ffcc;
text-shadow: 0 0 15px rgba(0, 255, 204, 0.6), 0 0 30px rgba(0, 255, 204, 0.2);
}
.ksmo-timer-controls {
display: flex;
gap: 0.8em;
}
.ksmo-timer-btn {
padding: 0.6em 1.4em;
font-size: 0.85em;
font-weight: 600;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.ksmo-timer-btn.start {
background: #00ffcc;
color: #0c0c14;
box-shadow: 0 0 12px rgba(0, 255, 204, 0.3);
}
.ksmo-timer-btn.pause {
background: #ffcc00;
color: #0c0c14;
box-shadow: 0 0 12px rgba(255, 204, 0, 0.3);
}
.ksmo-timer-btn.reset {
background: rgba(255, 255, 255, 0.08);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.ksmo-timer-btn:hover {
transform: translateY(-1px);
opacity: 0.9;
}
.ksmo-resize-e {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: e-resize;
z-index: 1000;
}
.ksmo-resize-s {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 6px;
cursor: s-resize;
z-index: 1000;
}
.ksmo-resize-se {
position: absolute;
bottom: 0;
right: 0;
width: 12px;
height: 12px;
cursor: se-resize;
z-index: 1001;
}
.ksmo-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(5, 5, 10, 0.85);
z-index: 1000000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
.ksmo-modal-overlay.show {
opacity: 1;
pointer-events: auto;
}
.ksmo-modal {
background: rgba(20, 20, 32, 0.96);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14px;
padding: 1.2em;
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.6);
width: 80%;
max-width: 240px;
display: flex;
flex-direction: column;
gap: 0.8em;
transform: scale(0.92);
transition: transform 0.2s ease;
}
.ksmo-modal-overlay.show .ksmo-modal {
transform: scale(1);
}
.ksmo-modal-title {
font-size: 0.95em;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
text-align: center;
}
.ksmo-modal-color-selectors {
display: flex;
justify-content: space-between;
gap: 0.4em;
}
.ksmo-modal-color-opt {
width: 1.5em;
height: 1.5em;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.ksmo-modal-color-opt.selected {
border-color: #fff;
transform: scale(1.15);
}
.ksmo-modal-actions {
display: flex;
gap: 0.5em;
}
.ksmo-modal-btn {
flex: 1;
padding: 0.6em 0;
border-radius: 8px;
border: none;
font-size: 0.85em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.ksmo-modal-btn.confirm {
background: #6c5ce7;
color: #fff;
}
.ksmo-modal-btn.cancel {
background: rgba(255, 255, 255, 0.06);
color: #ccc;
}
`;
shadow.appendChild(style);
const panel = document.createElement('div');
panel.className = 'ksmo-panel';
if (state.visible) {
panel.classList.add('show');
}
panel.style.left = `${state.posX}px`;
panel.style.top = `${state.posY}px`;
panel.style.width = `${state.width}px`;
panel.style.height = `${state.height}px`;
const header = document.createElement('div');
header.className = `ksmo-header${state.locked ? ' locked' : ''}`;
const titleEl = document.createElement('span');
titleEl.className = 'ksmo-title';
titleEl.textContent = 'Dashboard';
const headerActions = document.createElement('div');
headerActions.className = 'ksmo-header-actions';
const lockBtn = document.createElement('button');
lockBtn.className = 'ksmo-icon-btn';
lockBtn.textContent = state.locked ? '🔒' : '🔓';
lockBtn.title = state.locked ? 'Unlock Position' : 'Lock Position';
const closeBtn = document.createElement('button');
closeBtn.className = 'ksmo-icon-btn';
closeBtn.textContent = '✕';
closeBtn.title = 'Close Panel';
headerActions.appendChild(lockBtn);
headerActions.appendChild(closeBtn);
header.appendChild(titleEl);
header.appendChild(headerActions);
panel.appendChild(header);
const tabsContainer = document.createElement('div');
tabsContainer.className = 'ksmo-tabs';
const tabConfigs = [
{ id: 'counters', label: 'Counters' },
{ id: 'tasks', label: 'Tasks' },
{ id: 'timer', label: 'Timer' }
];
const tabElements = {};
tabConfigs.forEach(config => {
const tab = document.createElement('button');
tab.className = `ksmo-tab${state.activeTab === config.id ? ' active' : ''}`;
tab.textContent = config.label;
tab.addEventListener('click', () => {
switchTab(config.id);
});
tabsContainer.appendChild(tab);
tabElements[config.id] = tab;
});
panel.appendChild(tabsContainer);
const content = document.createElement('div');
content.className = 'ksmo-content';
panel.appendChild(content);
const views = {};
tabConfigs.forEach(config => {
const view = document.createElement('div');
view.className = `ksmo-view${state.activeTab === config.id ? ' active' : ''}`;
content.appendChild(view);
views[config.id] = view;
});
const updateFontSize = () => {
const baseWidth = 260;
const baseHeight = 380;
const baseFontSize = 13;
const scaleX = state.width / baseWidth;
const scaleY = state.height / baseHeight;
const scale = Math.min(scaleX, scaleY, 1.8);
panel.style.fontSize = `${Math.max(10, baseFontSize * scale)}px`;
};
updateFontSize();
const switchTab = (tabId) => {
state.activeTab = tabId;
saveState();
Object.keys(tabElements).forEach(id => {
tabElements[id].classList.toggle('active', id === tabId);
views[id].classList.toggle('active', id === tabId);
});
};
const showModal = (titleText, initValue, colors, onConfirm) => {
const overlay = document.createElement('div');
overlay.className = 'ksmo-modal-overlay';
const modal = document.createElement('div');
modal.className = 'ksmo-modal';
const title = document.createElement('div');
title.className = 'ksmo-modal-title';
title.textContent = titleText;
const input = document.createElement('input');
input.className = 'ksmo-input';
input.type = 'text';
input.value = initValue;
modal.appendChild(title);
modal.appendChild(input);
let selectedColor = colors ? colors[0] : null;
if (colors) {
const colorContainer = document.createElement('div');
colorContainer.className = 'ksmo-modal-color-selectors';
const optionEls = [];
colors.forEach(col => {
const opt = document.createElement('div');
opt.className = 'ksmo-modal-color-opt';
opt.style.backgroundColor = col;
if (col === selectedColor) opt.classList.add('selected');
opt.addEventListener('click', () => {
optionEls.forEach(el => el.classList.remove('selected'));
opt.classList.add('selected');
selectedColor = col;
});
colorContainer.appendChild(opt);
optionEls.push(opt);
});
modal.appendChild(colorContainer);
}
const actions = document.createElement('div');
actions.className = 'ksmo-modal-actions';
const cancel = document.createElement('button');
cancel.className = 'ksmo-modal-btn cancel';
cancel.textContent = 'Cancel';
cancel.addEventListener('click', () => close());
const confirm = document.createElement('button');
confirm.className = 'ksmo-modal-btn confirm';
confirm.textContent = 'Save';
confirm.addEventListener('click', () => {
const val = input.value.trim();
if (val) {
onConfirm(val, selectedColor);
close();
}
});
actions.appendChild(cancel);
actions.appendChild(confirm);
modal.appendChild(actions);
overlay.appendChild(modal);
panel.appendChild(overlay);
const close = () => {
overlay.classList.remove('show');
setTimeout(() => overlay.remove(), 200);
};
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
setTimeout(() => {
overlay.classList.add('show');
input.focus();
input.select();
}, 10);
};
const renderCounters = () => {
const view = views.counters;
view.innerHTML = '';
const list = document.createElement('div');
list.className = 'ksmo-counter-list';
state.counters.forEach(counter => {
const item = document.createElement('div');
item.className = 'ksmo-counter-item';
const info = document.createElement('div');
info.className = 'ksmo-counter-info';
info.addEventListener('click', () => {
showModal('Edit Counter Name', counter.name, null, (newName) => {
counter.name = newName;
saveState();
renderCounters();
});
});
const name = document.createElement('div');
name.className = 'ksmo-counter-name';
name.textContent = counter.name;
name.style.color = counter.color;
const val = document.createElement('div');
val.className = 'ksmo-counter-val';
val.textContent = counter.value;
val.style.color = counter.color;
val.style.textShadow = `0 0 10px ${counter.color}a0, 0 0 20px ${counter.color}50`;
info.appendChild(name);
info.appendChild(val);
const actions = document.createElement('div');
actions.className = 'ksmo-counter-actions';
const minus = document.createElement('button');
minus.className = 'ksmo-btn';
minus.textContent = '-';
minus.addEventListener('click', () => {
counter.value--;
val.textContent = counter.value;
saveState();
});
const reset = document.createElement('button');
reset.className = 'ksmo-btn';
reset.textContent = '↺';
reset.addEventListener('click', () => {
counter.value = 0;
val.textContent = counter.value;
saveState();
});
const plus = document.createElement('button');
plus.className = 'ksmo-btn';
plus.textContent = '+';
plus.addEventListener('click', () => {
counter.value++;
val.textContent = counter.value;
saveState();
});
const del = document.createElement('button');
del.className = 'ksmo-btn danger';
del.textContent = '✕';
del.addEventListener('click', () => {
state.counters = state.counters.filter(c => c.id !== counter.id);
saveState();
renderCounters();
});
actions.appendChild(minus);
actions.appendChild(reset);
actions.appendChild(plus);
if (state.counters.length > 1) {
actions.appendChild(del);
}
item.appendChild(info);
item.appendChild(actions);
list.appendChild(item);
});
view.appendChild(list);
const addBtn = document.createElement('button');
addBtn.className = 'ksmo-add-btn';
addBtn.textContent = '+ Add Counter';
addBtn.addEventListener('click', () => {
const availColors = ['#ff85ff', '#ffb834', '#00ffcc', '#3498db', '#e74c3c', '#2ecc71'];
showModal('Add Counter', `User ${state.counters.length + 1}`, availColors, (newName, color) => {
state.counters.push({
id: generateId(),
name: newName,
value: 0,
color: color
});
saveState();
renderCounters();
});
});
view.appendChild(addBtn);
};
const renderTasks = () => {
const view = views.tasks;
view.innerHTML = '';
const inputRow = document.createElement('div');
inputRow.className = 'ksmo-task-input-row';
const input = document.createElement('input');
input.className = 'ksmo-input';
input.type = 'text';
input.placeholder = 'New task...';
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') add();
});
const addBtn = document.createElement('button');
addBtn.className = 'ksmo-btn';
addBtn.textContent = '+';
const add = () => {
const text = input.value.trim();
if (text) {
state.tasks.push({
id: generateId(),
text: text,
completed: false
});
saveState();
renderTasks();
}
};
addBtn.addEventListener('click', add);
inputRow.appendChild(input);
inputRow.appendChild(addBtn);
view.appendChild(inputRow);
const list = document.createElement('div');
list.className = 'ksmo-task-list';
state.tasks.forEach(task => {
const item = document.createElement('div');
item.className = 'ksmo-task-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'ksmo-task-checkbox';
checkbox.checked = task.completed;
checkbox.addEventListener('change', () => {
task.completed = checkbox.checked;
text.classList.toggle('completed', task.completed);
saveState();
});
const text = document.createElement('span');
text.className = `ksmo-task-text${task.completed ? ' completed' : ''}`;
text.textContent = task.text;
const del = document.createElement('button');
del.className = 'ksmo-btn danger';
del.textContent = '✕';
del.addEventListener('click', () => {
state.tasks = state.tasks.filter(t => t.id !== task.id);
saveState();
renderTasks();
});
item.appendChild(checkbox);
item.appendChild(text);
item.appendChild(del);
list.appendChild(item);
});
view.appendChild(list);
};
const renderTimer = () => {
const view = views.timer;
view.innerHTML = '';
const containerEl = document.createElement('div');
containerEl.className = 'ksmo-timer-container';
const display = document.createElement('div');
display.className = 'ksmo-timer-display';
display.textContent = formatTime(timerSeconds);
timerDisplayEl = display;
const controls = document.createElement('div');
controls.className = 'ksmo-timer-controls';
const startPause = document.createElement('button');
startPause.className = `ksmo-timer-btn ${isTimerRunning ? 'pause' : 'start'}`;
startPause.textContent = isTimerRunning ? 'Pause' : 'Start';
const reset = document.createElement('button');
reset.className = 'ksmo-timer-btn reset';
reset.textContent = 'Reset';
startPause.addEventListener('click', () => {
if (isTimerRunning) {
clearInterval(timerInterval);
clearTimeout(timerInterval);
isTimerRunning = false;
startPause.textContent = 'Start';
startPause.className = 'ksmo-timer-btn start';
state.timer.running = false;
state.timer.seconds = timerSeconds;
state.timer.startedAt = null;
saveState();
} else {
state.timer.running = true;
state.timer.startedAt = Date.now();
saveState();
isTimerRunning = true;
startTimer();
startPause.textContent = 'Pause';
startPause.className = 'ksmo-timer-btn pause';
}
});
reset.addEventListener('click', () => {
clearInterval(timerInterval);
clearTimeout(timerInterval);
isTimerRunning = false;
timerSeconds = 0;
display.textContent = formatTime(timerSeconds);
startPause.textContent = 'Start';
startPause.className = 'ksmo-timer-btn start';
state.timer.running = false;
state.timer.seconds = 0;
state.timer.startedAt = null;
saveState();
});
controls.appendChild(startPause);
controls.appendChild(reset);
containerEl.appendChild(display);
containerEl.appendChild(controls);
view.appendChild(containerEl);
};
renderCounters();
renderTasks();
renderTimer();
const resizerE = document.createElement('div');
resizerE.className = 'ksmo-resize-e';
const resizerS = document.createElement('div');
resizerS.className = 'ksmo-resize-s';
const resizerSE = document.createElement('div');
resizerSE.className = 'ksmo-resize-se';
panel.appendChild(resizerE);
panel.appendChild(resizerS);
panel.appendChild(resizerSE);
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
let initialLeft = 0;
let initialTop = 0;
const onHeaderMouseDown = (e) => {
if (state.locked) return;
if (e.target.closest('.ksmo-icon-btn')) return;
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
initialLeft = state.posX;
initialTop = state.posY;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const newPos = clampPos(initialLeft + dx, initialTop + dy, state.width, state.height);
state.posX = newPos.x;
state.posY = newPos.y;
panel.style.left = `${state.posX}px`;
panel.style.top = `${state.posY}px`;
};
const onMouseUp = () => {
if (isDragging) {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
saveState();
}
};
header.addEventListener('mousedown', onHeaderMouseDown);
let isResizing = false;
let resizeType = '';
let resizeStartX = 0;
let resizeStartY = 0;
let initialWidth = 0;
let initialHeight = 0;
const startResize = (e, type) => {
isResizing = true;
resizeType = type;
resizeStartX = e.clientX;
resizeStartY = e.clientY;
initialWidth = state.width;
initialHeight = state.height;
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeUp);
e.preventDefault();
e.stopPropagation();
};
const onResizeMove = (e) => {
if (!isResizing) return;
const dx = e.clientX - resizeStartX;
const dy = e.clientY - resizeStartY;
let newWidth = initialWidth;
let newHeight = initialHeight;
if (resizeType.includes('e')) {
newWidth = Math.max(220, initialWidth + dx);
}
if (resizeType.includes('s')) {
newHeight = Math.max(300, initialHeight + dy);
}
if (state.posX + newWidth > window.innerWidth) {
newWidth = window.innerWidth - state.posX;
}
if (state.posY + newHeight > window.innerHeight) {
newHeight = window.innerHeight - state.posY;
}
state.width = newWidth;
state.height = newHeight;
panel.style.width = `${newWidth}px`;
panel.style.height = `${newHeight}px`;
updateFontSize();
};
const onResizeUp = () => {
if (isResizing) {
isResizing = false;
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeUp);
saveState();
}
};
resizerE.addEventListener('mousedown', (e) => startResize(e, 'e'));
resizerS.addEventListener('mousedown', (e) => startResize(e, 's'));
resizerSE.addEventListener('mousedown', (e) => startResize(e, 'se'));
lockBtn.addEventListener('mousedown', (e) => {
e.stopPropagation();
});
lockBtn.addEventListener('click', () => {
state.locked = !state.locked;
lockBtn.textContent = state.locked ? '🔒' : '🔓';
lockBtn.title = state.locked ? 'Unlock Position' : 'Lock Position';
header.classList.toggle('locked', state.locked);
saveState();
});
const closePanel = () => {
state.visible = false;
saveState();
panel.classList.remove('show');
};
const openPanel = () => {
state.visible = true;
saveState();
panel.classList.add('show');
};
closeBtn.addEventListener('click', closePanel);
const pin = document.createElement('div');
pin.className = 'ksmo-pin';
pin.textContent = '📊';
pin.style.top = `${state.pinY}px`;
const pinStyle = document.createElement('style');
pinStyle.textContent = `
.ksmo-pin {
position: fixed;
right: 0;
z-index: 2147483647;
width: 44px;
height: 44px;
background: rgba(15, 15, 25, 0.88);
border-radius: 12px 0 0 12px;
box-shadow: -4px 4px 20px rgba(0,0,0,0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
border-right: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: grab;
transition: transform 0.2s ease, opacity 0.3s ease;
transform: translateX(34px);
opacity: 0;
pointer-events: none;
user-select: none;
}
.ksmo-pin.visible {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.ksmo-pin.active {
cursor: grabbing;
}
`;
document.head.appendChild(pinStyle);
const BORDER_THRESHOLD = Math.min(50, window.innerWidth * 0.03);
let hidePinTimeout = null;
const showPin = () => {
clearTimeout(hidePinTimeout);
pin.classList.add('visible');
};
const hidePin = () => {
clearTimeout(hidePinTimeout);
hidePinTimeout = setTimeout(() => {
if (!pin.classList.contains('active') && !pin._hovered) {
pin.classList.remove('visible');
}
}, 1200);
};
document.addEventListener('mousemove', (e) => {
const fromRight = window.innerWidth - e.clientX;
if (fromRight <= BORDER_THRESHOLD) {
showPin();
} else {
hidePin();
}
});
pin.addEventListener('mouseenter', () => {
pin._hovered = true;
showPin();
});
pin.addEventListener('mouseleave', () => {
pin._hovered = false;
hidePin();
});
panel.addEventListener('mouseenter', () => {
pin._hovered = true;
showPin();
});
panel.addEventListener('mouseleave', () => {
pin._hovered = false;
hidePin();
});
let isPinDragging = false;
let pinDragStartY = 0;
let pinStartTop = 0;
pin.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isPinDragging = true;
pin.classList.add('active');
pinDragStartY = e.clientY;
pinStartTop = parseInt(pin.style.top, 10);
document.addEventListener('mousemove', onPinMouseMove);
document.addEventListener('mouseup', onPinMouseUp);
e.preventDefault();
e.stopPropagation();
});
const onPinMouseMove = (e) => {
if (!isPinDragging) return;
const dy = e.clientY - pinDragStartY;
const pinHeight = pin.offsetHeight || 44;
let newTop = Math.max(0, Math.min(window.innerHeight - pinHeight, pinStartTop + dy));
state.pinY = newTop;
pin.style.top = `${newTop}px`;
};
const onPinMouseUp = () => {
if (isPinDragging) {
isPinDragging = false;
pin.classList.remove('active');
document.removeEventListener('mousemove', onPinMouseMove);
document.removeEventListener('mouseup', onPinMouseUp);
saveState();
hidePin();
}
};
pin.addEventListener('click', (e) => {
if (isPinDragging) return;
e.stopPropagation();
if (state.visible) {
closePanel();
} else {
openPanel();
}
});
const onResizeWindow = () => {
const newPos = clampPos(state.posX, state.posY, state.width, state.height);
state.posX = newPos.x;
state.posY = newPos.y;
panel.style.left = `${state.posX}px`;
panel.style.top = `${state.posY}px`;
const pinHeight = pin.offsetHeight || 44;
let newPinY = Math.max(0, Math.min(window.innerHeight - pinHeight, state.pinY));
state.pinY = newPinY;
pin.style.top = `${newPinY}px`;
saveState();
};
window.addEventListener('resize', onResizeWindow);
shadow.appendChild(panel);
const initApp = () => {
document.body.appendChild(host);
document.body.appendChild(pin);
if (state.visible) {
openPanel();
}
};
if (!document.body) {
new MutationObserver((_, obs) => {
if (document.body) {
initApp();
obs.disconnect();
}
}).observe(document.documentElement, { childList: true });
} else {
initApp();
}
showPin();
hidePin();
})();