diff --git a/manifest.json b/manifest.json
index 88c82a1..b0e6348 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,9 +1,9 @@
{
"id": "calm-focus-log",
"name": "Calm Focus Log",
- "version": "1.0.10",
+ "version": "1.1.0",
"minAppVersion": "1.0.0",
- "description": "Log focus sessions on a 90-day heatmap. No goals or timers — just a calm record of what you did.",
+ "description": "Log focus sessions on a 90-day heatmap. Pre or post logging with optional countdown timer.",
"author": "Kunal1522",
"authorUrl": "https://github.com/Kunal1522",
"isDesktopOnly": true
diff --git a/tracker.html b/tracker.html
index 6618254..241f339 100644
--- a/tracker.html
+++ b/tracker.html
@@ -211,7 +211,7 @@
}
.cell-inner.both .cell-zen {
- width: 28%;
+ width: 34%;
}
.cell-inner.both .cell-mins {
@@ -470,55 +470,49 @@
box-shadow: 0 0 0 3px rgba(196, 105, 90, 0.15);
}
- .slider-row {
+ .mode-toggle {
display: flex;
- align-items: center;
- gap: 0.75rem;
+ gap: 0.35rem;
}
- .slider-row input[type="range"] {
+ .mode-btn {
flex: 1;
- height: 6px;
- appearance: none;
background: var(--surface-inset);
border: 1px solid var(--border);
- border-radius: var(--radius-pill);
- cursor: pointer;
- }
-
- .slider-row input[type="range"]::-webkit-slider-thumb {
- appearance: none;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: var(--accent);
- border: 2px solid var(--surface);
- box-shadow: var(--shadow);
- }
-
- .slider-row input[type="range"]::-moz-range-thumb {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: var(--accent);
- border: 2px solid var(--surface);
- box-shadow: var(--shadow);
- }
-
- .slider-val {
+ color: var(--text-dim);
+ font-family: inherit;
font-size: 0.78rem;
- font-weight: 600;
- color: var(--text-dim);
- min-width: 2.5rem;
- text-align: right;
+ 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;
}
- .slider-ticks {
- display: flex;
- justify-content: space-between;
- padding: 0.25rem 0.5rem 0;
- font-size: 0.65rem;
- color: var(--text-dim);
+ .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 {
@@ -577,15 +571,10 @@
-
-
-
- 100%
-
-
-
80
-
90
-
100
+
+
+
+
@@ -615,6 +604,8 @@
let data = {};
let selectedDate = todayStr();
let lastKnownToday = todayStr();
+ let logMode = 'post';
+ let timerInterval = null;
const inPlugin = window.parent !== window;
// ── storage ──
@@ -638,7 +629,9 @@
function boot(initial) {
data = initial || loadLocal();
+ recoverPreSessions();
renderAll();
+ if (anyActivePreSessions()) ensureTimerLoop();
}
function uid() {
@@ -699,6 +692,11 @@
}
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;
@@ -708,8 +706,97 @@
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 + effectiveDur(s), 0);
+ return daySessions(dateStr).reduce((sum, s) => sum + heatmapDur(s), 0);
}
function maxMinutesInRange(dates) {
@@ -895,7 +982,7 @@
function renderDayDetail() {
document.getElementById('selected-date-label').textContent = fmtDisplayDate(selectedDate);
const sessions = daySessions(selectedDate);
- const total = Math.round(sessions.reduce((s, x) => s + effectiveDur(x), 0));
+ const total = Math.round(sessions.reduce((s, x) => s + heatmapDur(x), 0));
document.getElementById('day-total').textContent =
total > 0 ? `${total} min` : '';
@@ -911,10 +998,12 @@
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';
+ card.className = 'session-card' + (active ? ' timing' : '');
+ card.dataset.sessionId = session.id;
const track = document.createElement('div');
track.className = 'session-track';
@@ -944,7 +1033,9 @@
const dur = document.createElement('div');
dur.className = 'session-dur';
- dur.textContent = session.dur + ' min';
+ dur.textContent = active
+ ? fmtCountdown(getPreRemaining(session))
+ : session.dur + ' min';
const del = document.createElement('button');
del.className = 'session-delete';
@@ -971,13 +1062,35 @@
// ── add session ──
- function addSession(label, dur, pct, dateStr) {
+ 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] = [];
- data[target].push({ id: uid(), label, dur: Number(dur), pct: Number(pct) });
+
+ 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 ──
@@ -988,18 +1101,15 @@
refreshCalendar();
document.getElementById('session-label').value = '';
document.getElementById('session-dur').value = '25';
- document.getElementById('session-pct').value = '100';
- document.getElementById('session-pct-val').textContent = '100%';
+ setLogMode('post');
setDateInputBounds();
syncDateInput(todayStr());
addModal.classList.add('active');
document.getElementById('session-label').focus();
});
- document.getElementById('session-pct').addEventListener('input', () => {
- document.getElementById('session-pct-val').textContent =
- document.getElementById('session-pct').value + '%';
- });
+ 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();
@@ -1013,9 +1123,8 @@
document.getElementById('log-it-btn').addEventListener('click', () => {
const label = document.getElementById('session-label').value.trim();
const dur = document.getElementById('session-dur').value;
- const pct = document.getElementById('session-pct').value;
if (!dur || dur < 1) return;
- addSession(label, dur, pct, getLogDate());
+ addSession(label, dur, logMode, getLogDate());
addModal.classList.remove('active');
});
diff --git a/versions.json b/versions.json
index 589a43a..2bf3fda 100644
--- a/versions.json
+++ b/versions.json
@@ -9,5 +9,6 @@
"1.0.7": "1.0.0",
"1.0.8": "1.0.0",
"1.0.9": "1.0.0",
- "1.0.10": "1.0.0"
+ "1.0.10": "1.0.0",
+ "1.1.0": "1.0.0"
}