kunal1522_obsidian-focus-tr.../tracker.html

1161 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>calm focus log</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #e8e4dc;
--surface: #f2efe8;
--surface-inset: #ddd8cf;
--text: #3d3429;
--text-dim: #7a7064;
--border: #c8c0b4;
--accent: #c4695a;
--accent-hover: #b05548;
--heatmap-empty: #e0dcd4;
--heatmap-light: #d4dfc9;
--heatmap-mid: #a3b896;
--heatmap-full: #6b8f5e;
--session-color: #8fa67a;
--shadow: 0 2px 8px rgba(61, 52, 41, 0.08), 0 1px 2px rgba(61, 52, 41, 0.06);
--shadow-lg: 0 8px 24px rgba(61, 52, 41, 0.12), 0 2px 6px rgba(61, 52, 41, 0.06);
--radius: 16px;
--radius-sm: 10px;
--radius-pill: 999px;
}
html, body {
min-height: 100%;
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.5;
}
body {
padding: 2rem 1rem 3rem;
display: flex;
justify-content: center;
}
.app-window {
width: 100%;
max-width: 520px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
box-shadow: var(--shadow-lg);
padding: 1.5rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.15rem;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.01em;
}
.btn {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
font-family: inherit;
font-size: 0.82rem;
font-weight: 500;
padding: 0.45rem 1rem;
border-radius: var(--radius-pill);
cursor: pointer;
box-shadow: var(--shadow);
transition: background 0.15s, transform 0.1s;
}
.btn:hover {
background: var(--surface-inset);
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: var(--accent);
border-color: var(--accent-hover);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-dim {
background: var(--surface-inset);
color: var(--text-dim);
box-shadow: none;
}
.btn-dim:hover {
color: var(--text);
background: var(--surface-inset);
}
/* heatmap card */
.heatmap-section {
margin-bottom: 1.25rem;
}
.heatmap-card {
background: var(--surface-inset);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
box-shadow: inset 0 1px 3px rgba(61, 52, 41, 0.06);
}
.heatmap-wrap {
width: 100%;
overflow: visible;
padding: 3px;
}
.heatmap {
display: grid;
grid-template-rows: repeat(7, 1fr);
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 4px;
width: 100%;
aspect-ratio: var(--heatmap-cols, 13) / 7.1;
}
.cell {
position: relative;
width: 100%;
min-width: 0;
min-height: 0;
border-radius: 4px;
border: 1px solid rgba(61, 52, 41, 0.08);
cursor: pointer;
transition: box-shadow 0.15s, border-color 0.15s;
background: var(--heatmap-empty);
container-type: inline-size;
}
.cell:hover {
border-color: var(--text-dim);
box-shadow: inset 0 0 0 1px var(--text-dim);
}
.cell.selected {
border-color: var(--accent);
box-shadow: inset 0 0 0 2px var(--accent);
}
.cell.today {
outline: 2px solid var(--text-dim);
outline-offset: 1px;
}
.cell.pad {
visibility: hidden;
pointer-events: none;
}
.cell-zen {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 50%;
pointer-events: none;
}
.cell-zen svg {
width: 100%;
height: auto;
filter: drop-shadow(0 0 1px rgba(61, 52, 41, 0.35));
}
.cell-inner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
padding: 2px;
pointer-events: none;
}
.cell-mins {
font-size: clamp(10px, 36cqi, 16px);
font-weight: 800;
line-height: 1;
color: #1f1a14;
letter-spacing: -0.04em;
text-shadow:
0 0 4px rgba(242, 239, 232, 0.95),
0 1px 0 rgba(242, 239, 232, 0.6);
}
.cell-inner.both .cell-zen {
width: 34%;
}
.cell-inner.both .cell-mins {
font-size: clamp(9px, 32cqi, 14px);
}
.meditation-row {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
padding: 0.5rem 0.65rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow);
}
.meditation-row.vanish {
animation: meditation-vanish 0.55s ease forwards;
}
@keyframes meditation-vanish {
0% { opacity: 1; transform: scale(1); max-height: 48px; margin-bottom: 0.75rem; }
100% { opacity: 0; transform: scale(0.92); max-height: 0; margin-bottom: 0; padding: 0; border-width: 0; }
}
.meditation-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
color: var(--text-dim);
cursor: pointer;
user-select: none;
}
.meditation-label input {
width: 15px;
height: 15px;
accent-color: var(--accent);
cursor: pointer;
}
.heatmap-meta {
margin-top: 0.75rem;
font-size: 0.75rem;
color: var(--text-dim);
text-align: center;
}
/* day detail */
.day-detail {
background: var(--surface-inset);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.1rem;
min-height: 100px;
}
.day-detail-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.85rem;
gap: 0.5rem;
}
.day-detail h2 {
font-size: 0.9rem;
font-weight: 600;
color: var(--accent);
}
.day-total {
font-size: 0.78rem;
color: var(--text-dim);
font-weight: 500;
white-space: nowrap;
}
.sessions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.session-card {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
box-shadow: var(--shadow);
}
.session-track {
position: absolute;
left: 0;
top: 0;
bottom: 0;
min-width: 4px;
opacity: 0.1;
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
pointer-events: none;
}
.session-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
min-width: 4px;
opacity: 0.5;
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
pointer-events: none;
transition: width 0.2s ease;
}
.session-content {
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.65rem;
}
.session-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.session-info {
flex: 1;
min-width: 0;
}
.session-name {
font-size: 0.82rem;
font-weight: 500;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-dur {
font-size: 0.72rem;
font-weight: 600;
color: var(--text);
background: var(--surface-inset);
padding: 0.15rem 0.5rem;
border-radius: var(--radius-pill);
border: 1px solid var(--border);
white-space: nowrap;
flex-shrink: 0;
}
.session-delete {
background: none;
border: none;
color: var(--text-dim);
font-family: inherit;
font-size: 1rem;
line-height: 1;
cursor: pointer;
padding: 0.15rem 0.25rem;
border-radius: 6px;
opacity: 0;
transition: opacity 0.15s, color 0.15s, background 0.15s;
flex-shrink: 0;
}
.session-card:hover .session-delete {
opacity: 1;
}
.session-delete:hover {
color: var(--accent);
background: rgba(196, 105, 90, 0.1);
}
@media (hover: none) {
.session-delete { opacity: 0.6; }
}
.empty-day {
color: var(--text-dim);
font-size: 0.82rem;
text-align: center;
padding: 0.5rem 0;
}
/* modals */
.overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(61, 52, 41, 0.35);
z-index: 100;
align-items: center;
justify-content: center;
padding: 1rem;
}
.overlay.active { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.35rem;
width: 100%;
max-width: 360px;
box-shadow: var(--shadow-lg);
}
.modal h3 {
font-size: 0.95rem;
font-weight: 600;
color: var(--accent);
margin-bottom: 1.1rem;
}
.form-group {
margin-bottom: 0.85rem;
}
.form-group label {
display: block;
font-size: 0.72rem;
font-weight: 500;
color: var(--text-dim);
margin-bottom: 0.3rem;
text-transform: lowercase;
}
.form-group input,
.form-group select {
width: 100%;
background: var(--surface-inset);
border: 1px solid var(--border);
color: var(--text);
font-family: inherit;
font-size: 0.85rem;
padding: 0.5rem 0.65rem;
border-radius: var(--radius-sm);
color-scheme: light;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(196, 105, 90, 0.15);
}
.mode-toggle {
display: flex;
gap: 0.35rem;
}
.mode-btn {
flex: 1;
background: var(--surface-inset);
border: 1px solid var(--border);
color: var(--text-dim);
font-family: inherit;
font-size: 0.78rem;
font-weight: 500;
padding: 0.42rem 0.65rem;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.mode-btn:hover {
color: var(--text);
background: var(--surface);
}
.mode-btn.active {
background: var(--surface);
border-color: var(--accent);
color: var(--accent);
box-shadow: 0 0 0 2px rgba(196, 105, 90, 0.12);
}
.session-card.timing {
border-color: rgba(107, 143, 94, 0.45);
}
.session-card.timing .session-dur {
color: var(--accent);
border-color: rgba(196, 105, 90, 0.35);
font-variant-numeric: tabular-nums;
}
.session-card.timing .session-fill {
opacity: 0.65;
}
.form-actions {
display: flex;
gap: 0.45rem;
margin-top: 1.1rem;
flex-wrap: wrap;
}
.form-actions .btn { flex: 1; min-width: 100px; }
</style>
</head>
<body>
<div class="app-window">
<header>
<h1>calm focus log</h1>
<button class="btn btn-primary" id="add-btn">+ session</button>
</header>
<section class="heatmap-section">
<div class="heatmap-card">
<div class="heatmap-wrap">
<div class="heatmap" id="heatmap"></div>
</div>
<div class="heatmap-meta" id="heatmap-meta"></div>
</div>
</section>
<section class="day-detail" id="day-detail">
<div class="day-detail-header">
<h2 id="selected-date-label"></h2>
<div class="day-total" id="day-total"></div>
</div>
<div id="meditation-slot"></div>
<div class="sessions" id="sessions"></div>
</section>
</div>
<!-- add session modal -->
<div class="overlay" id="add-modal">
<div class="modal">
<h3>log session</h3>
<div class="form-group">
<label for="session-date">date</label>
<input type="date" id="session-date">
</div>
<div class="form-group">
<label for="session-label">label</label>
<input type="text" id="session-label" placeholder="..." autocomplete="off">
</div>
<div class="form-group">
<label for="session-dur">duration (min)</label>
<input type="number" id="session-dur" min="1" max="480" value="25">
</div>
<div class="form-group">
<label>when</label>
<div class="mode-toggle">
<button type="button" class="mode-btn active" data-mode="post" id="mode-post">post</button>
<button type="button" class="mode-btn" data-mode="pre" id="mode-pre">pre</button>
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary" id="log-it-btn">log</button>
<button class="btn btn-dim" id="cancel-add-btn">cancel</button>
</div>
</div>
</div>
<script>
(function () {
'use strict';
const STORAGE_KEY = 'focus-tracker-data';
const DAYS = 90;
const HEATMAP_ROWS = 7;
const SESSION_COLOR = '#6b8f5e';
const META_MEDITATION = '_meditation';
const YIN_YANG_SVG = '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">'
+ '<circle cx="16" cy="16" r="15" fill="#f2efe8" stroke="#3d3429" stroke-width="1"/>'
+ '<path d="M16 1a15 15 0 0 1 0 30c-4 0-7.5-3.3-7.5-7.5S12 16 16 16s7.5-3.5 7.5-7.5S20 1 16 1z" fill="#3d3429"/>'
+ '<circle cx="16" cy="8.5" r="2.2" fill="#f2efe8"/>'
+ '<circle cx="16" cy="23.5" r="2.2" fill="#3d3429"/>'
+ '</svg>';
let data = {};
let selectedDate = todayStr();
let lastKnownToday = todayStr();
let logMode = 'post';
let timerInterval = null;
const inPlugin = window.parent !== window;
// ── storage ──
function loadLocal() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch {
return {};
}
}
function saveData() {
if (inPlugin) {
window.parent.postMessage({ source: 'focus-tracker', type: 'save', payload: data }, '*');
} else {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
}
function boot(initial) {
data = initial || loadLocal();
recoverPreSessions();
renderAll();
if (anyActivePreSessions()) ensureTimerLoop();
}
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
// ── dates ──
function todayStr() {
return fmtDate(new Date());
}
function tickCalendar() {
const today = todayStr();
if (today === lastKnownToday) return false;
lastKnownToday = today;
selectedDate = today;
return true;
}
function refreshCalendar() {
if (tickCalendar()) renderAll();
}
function fmtDate(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
function parseDate(s) {
const [y, m, d] = s.split('-').map(Number);
return new Date(y, m - 1, d);
}
function addDays(d, n) {
const r = new Date(d);
r.setDate(r.getDate() + n);
return r;
}
function daySessions(dateStr) {
const v = data[dateStr];
return Array.isArray(v) ? v : [];
}
function isMeditated(dateStr) {
return !!(data[META_MEDITATION] && data[META_MEDITATION][dateStr]);
}
function setMeditated(dateStr, on) {
if (!data[META_MEDITATION]) data[META_MEDITATION] = {};
if (on) data[META_MEDITATION][dateStr] = true;
else delete data[META_MEDITATION][dateStr];
if (Object.keys(data[META_MEDITATION]).length === 0) delete data[META_MEDITATION];
saveData();
}
function sessionPct(s) {
if (isPreActive(s)) {
const total = s.dur * 60;
const elapsed = total - getPreRemaining(s);
return total > 0 ? Math.min(100, (elapsed / total) * 100) : 0;
}
if (s.pct != null) return Number(s.pct) || 100;
if (s.status === 'incomplete') return 80;
return 100;
}
function effectiveDur(s) {
return s.dur * sessionPct(s) / 100;
}
function getPreRemaining(s) {
if (s.mode !== 'pre' || !s.startedAt) return null;
const elapsed = (Date.now() - s.startedAt) / 1000;
return Math.max(0, s.dur * 60 - elapsed);
}
function isPreActive(s) {
return s.mode === 'pre' && s.startedAt && getPreRemaining(s) > 0;
}
function anyActivePreSessions() {
for (const dateStr in data) {
if (dateStr === META_MEDITATION) continue;
for (const s of daySessions(dateStr)) {
if (isPreActive(s)) return true;
}
}
return false;
}
function recoverPreSessions() {
let changed = false;
for (const dateStr in data) {
if (dateStr === META_MEDITATION) continue;
for (const s of daySessions(dateStr)) {
if (s.mode === 'pre' && s.startedAt && getPreRemaining(s) <= 0) {
delete s.mode;
delete s.startedAt;
s.pct = 100;
changed = true;
}
}
}
if (changed) saveData();
}
function fmtCountdown(totalSec) {
const m = Math.floor(totalSec / 60);
const s = Math.floor(totalSec % 60);
return `${m}:${String(s).padStart(2, '0')}`;
}
function updateTimerDisplays() {
document.querySelectorAll('.session-card[data-session-id]').forEach(card => {
const session = daySessions(selectedDate).find(s => s.id === card.dataset.sessionId);
if (!session || !isPreActive(session)) return;
const rem = getPreRemaining(session);
const durEl = card.querySelector('.session-dur');
const fill = card.querySelector('.session-fill');
if (durEl) durEl.textContent = fmtCountdown(rem);
if (fill) fill.style.width = sessionPct(session) + '%';
});
}
function tickTimers() {
let completed = false;
for (const dateStr in data) {
if (dateStr === META_MEDITATION) continue;
for (const s of daySessions(dateStr)) {
if (s.mode === 'pre' && s.startedAt && getPreRemaining(s) <= 0) {
delete s.mode;
delete s.startedAt;
s.pct = 100;
completed = true;
}
}
}
if (completed) {
saveData();
renderAll();
return;
}
if (anyActivePreSessions()) updateTimerDisplays();
}
function ensureTimerLoop() {
if (timerInterval) return;
timerInterval = setInterval(tickTimers, 1000);
}
function heatmapDur(s) {
if (s.mode === 'pre') return s.dur;
return effectiveDur(s);
}
function dayMinutes(dateStr) {
return daySessions(dateStr).reduce((sum, s) => sum + heatmapDur(s), 0);
}
function maxMinutesInRange(dates) {
let max = 0;
for (const d of dates) {
const m = dayMinutes(d);
if (m > max) max = m;
}
return max || 1;
}
function cellColor(mins, maxMins) {
if (mins === 0) return '#e0dcd4';
const t = Math.min(mins / maxMins, 1);
if (t <= 0.33) return lerpColor('#e0dcd4', '#d4dfc9', t / 0.33);
if (t <= 0.66) return lerpColor('#d4dfc9', '#a3b896', (t - 0.33) / 0.33);
return lerpColor('#a3b896', '#6b8f5e', (t - 0.66) / 0.34);
}
function lerpColor(a, b, t) {
const pa = hexRgb(a), pb = hexRgb(b);
const r = Math.round(pa.r + (pb.r - pa.r) * t);
const g = Math.round(pa.g + (pb.g - pa.g) * t);
const bl = Math.round(pa.b + (pb.b - pa.b) * t);
return `rgb(${r},${g},${bl})`;
}
function hexRgb(h) {
const n = parseInt(h.slice(1), 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
function fmtDisplayDate(s) {
const d = parseDate(s);
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'];
return `${days[d.getDay()]} ${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
}
// ── render heatmap ──
function getDateRange() {
const end = new Date();
end.setHours(0, 0, 0, 0);
const start = addDays(end, -(DAYS - 1));
const dates = [];
for (let i = 0; i < DAYS; i++) {
dates.push(fmtDate(addDays(start, i)));
}
return dates;
}
function renderHeatmap() {
const dates = getDateRange();
const maxMins = maxMinutesInRange(dates);
const container = document.getElementById('heatmap');
container.innerHTML = '';
const cols = Math.ceil(DAYS / HEATMAP_ROWS);
const padCount = HEATMAP_ROWS * cols - DAYS;
// fixed grid: oldest near top-left, today always bottom-right
for (let i = 0; i < padCount; i++) {
const pad = document.createElement('div');
pad.className = 'cell pad';
container.appendChild(pad);
}
const today = todayStr();
container.style.setProperty('--heatmap-cols', cols);
dates.forEach(dateStr => {
const mins = Math.round(dayMinutes(dateStr));
const meditated = isMeditated(dateStr);
const cell = document.createElement('div');
cell.className = 'cell';
cell.style.background = cellColor(mins, maxMins);
cell.title = `${dateStr}: ${mins} min${meditated ? ' · meditated' : ''}`;
cell.dataset.date = dateStr;
if (dateStr === selectedDate) cell.classList.add('selected');
if (dateStr === today) cell.classList.add('today');
if (meditated) cell.classList.add('meditated');
if (mins > 0 || meditated) {
const inner = document.createElement('div');
inner.className = 'cell-inner' + (mins > 0 && meditated ? ' both' : '');
if (mins > 0) {
const num = document.createElement('span');
num.className = 'cell-mins';
num.textContent = mins;
inner.appendChild(num);
}
if (meditated) {
const zen = document.createElement('div');
zen.className = 'cell-zen';
zen.innerHTML = YIN_YANG_SVG;
inner.appendChild(zen);
}
cell.appendChild(inner);
}
cell.addEventListener('click', () => selectDate(dateStr));
container.appendChild(cell);
});
const totalMins = dates.reduce((s, d) => s + dayMinutes(d), 0);
document.getElementById('heatmap-meta').textContent =
`${DAYS} days · ${totalMins} min total`;
}
// ── render day detail ──
function selectDate(dateStr) {
selectedDate = dateStr;
document.querySelectorAll('.cell.selected').forEach(c => c.classList.remove('selected'));
const cell = document.querySelector(`.cell[data-date="${dateStr}"]`);
if (cell) cell.classList.add('selected');
syncDateInput(dateStr);
renderDayDetail();
}
function syncDateInput(dateStr) {
const el = document.getElementById('session-date');
if (el) el.value = dateStr;
}
function getLogDate() {
const el = document.getElementById('session-date');
return (el && el.value) ? el.value : selectedDate;
}
function setDateInputBounds() {
const el = document.getElementById('session-date');
const today = new Date();
today.setHours(0, 0, 0, 0);
el.max = fmtDate(today);
el.min = fmtDate(addDays(today, -(DAYS - 1)));
}
function deleteSession(dateStr, sessionId) {
const sessions = daySessions(dateStr);
if (!sessions.length) return;
data[dateStr] = sessions.filter(s => s.id !== sessionId);
if (data[dateStr].length === 0) delete data[dateStr];
saveData();
renderAll();
}
function renderMeditationRow() {
const slot = document.getElementById('meditation-slot');
slot.innerHTML = '';
if (isMeditated(selectedDate)) return;
const row = document.createElement('div');
row.className = 'meditation-row';
const label = document.createElement('label');
label.className = 'meditation-label';
const check = document.createElement('input');
check.type = 'checkbox';
check.addEventListener('change', () => {
if (!check.checked) return;
row.classList.add('vanish');
row.addEventListener('animationend', () => {
setMeditated(selectedDate, true);
renderAll();
}, { once: true });
});
const text = document.createElement('span');
text.textContent = 'meditate?';
label.appendChild(check);
label.appendChild(text);
row.appendChild(label);
slot.appendChild(row);
}
function renderDayDetail() {
document.getElementById('selected-date-label').textContent = fmtDisplayDate(selectedDate);
const sessions = daySessions(selectedDate);
const total = Math.round(sessions.reduce((s, x) => s + heatmapDur(x), 0));
document.getElementById('day-total').textContent =
total > 0 ? `${total} min` : '';
renderMeditationRow();
const container = document.getElementById('sessions');
container.innerHTML = '';
if (sessions.length === 0) {
container.innerHTML = '<div class="empty-day">—</div>';
return;
}
sessions.forEach(session => {
const name = session.label || 'session';
const active = isPreActive(session);
const pct = Math.min(100, Math.max(0, sessionPct(session)));
const card = document.createElement('div');
card.className = 'session-card' + (active ? ' timing' : '');
card.dataset.sessionId = session.id;
const track = document.createElement('div');
track.className = 'session-track';
track.style.width = '100%';
track.style.background = SESSION_COLOR;
const fill = document.createElement('div');
fill.className = 'session-fill';
fill.style.width = pct + '%';
fill.style.background = SESSION_COLOR;
const content = document.createElement('div');
content.className = 'session-content';
const dot = document.createElement('div');
dot.className = 'session-dot';
dot.style.background = SESSION_COLOR;
const info = document.createElement('div');
info.className = 'session-info';
const nameEl = document.createElement('div');
nameEl.className = 'session-name';
nameEl.textContent = name;
info.appendChild(nameEl);
const dur = document.createElement('div');
dur.className = 'session-dur';
dur.textContent = active
? fmtCountdown(getPreRemaining(session))
: session.dur + ' min';
const del = document.createElement('button');
del.className = 'session-delete';
del.textContent = '×';
del.title = 'delete';
del.addEventListener('click', () => deleteSession(selectedDate, session.id));
content.appendChild(dot);
content.appendChild(info);
content.appendChild(dur);
content.appendChild(del);
card.appendChild(track);
card.appendChild(fill);
card.appendChild(content);
container.appendChild(card);
});
}
function renderAll() {
lastKnownToday = todayStr();
renderHeatmap();
renderDayDetail();
}
// ── add session ──
function setLogMode(mode) {
logMode = mode === 'pre' ? 'pre' : 'post';
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === logMode);
});
}
function addSession(label, dur, mode, dateStr) {
const target = dateStr || getLogDate();
if (!Array.isArray(data[target])) data[target] = [];
const session = {
id: uid(),
label,
dur: Number(dur)
};
if (mode === 'pre') {
session.mode = 'pre';
session.startedAt = Date.now();
} else {
session.pct = 100;
}
data[target].push(session);
selectedDate = target;
saveData();
renderAll();
if (mode === 'pre') ensureTimerLoop();
}
// ── modals ──
const addModal = document.getElementById('add-modal');
document.getElementById('add-btn').addEventListener('click', () => {
refreshCalendar();
document.getElementById('session-label').value = '';
document.getElementById('session-dur').value = '25';
setLogMode('post');
setDateInputBounds();
syncDateInput(todayStr());
addModal.classList.add('active');
document.getElementById('session-label').focus();
});
document.getElementById('mode-post').addEventListener('click', () => setLogMode('post'));
document.getElementById('mode-pre').addEventListener('click', () => setLogMode('pre'));
document.getElementById('session-date').addEventListener('change', () => {
const d = getLogDate();
if (d) selectDate(d);
});
document.getElementById('cancel-add-btn').addEventListener('click', () => {
addModal.classList.remove('active');
});
document.getElementById('log-it-btn').addEventListener('click', () => {
const label = document.getElementById('session-label').value.trim();
const dur = document.getElementById('session-dur').value;
if (!dur || dur < 1) return;
addSession(label, dur, logMode, getLogDate());
addModal.classList.remove('active');
});
addModal.addEventListener('click', e => {
if (e.target === addModal) addModal.classList.remove('active');
});
// keyboard: escape closes add modal
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (addModal.classList.contains('active')) addModal.classList.remove('active');
}
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) refreshCalendar();
});
window.addEventListener('focus', refreshCalendar);
setInterval(refreshCalendar, 60000);
if (inPlugin) {
window.addEventListener('message', (e) => {
if (e.data?.source !== 'focus-tracker-plugin') return;
if (e.data.type === 'init') boot(e.data.result);
});
window.parent.postMessage({ source: 'focus-tracker', type: 'ready' }, '*');
} else {
boot();
}
})();
</script>
</body>
</html>