☀️ My View
your day at a glance — focused, clear, doable
⚡ next 30 minutes
00:00
Reply to Sarah's email
10 min
Review meeting notes
15 min
Take medication
5 min
how's your brain right now?
❤️ health today
7h
sleep
4.8k
steps
62
HRV
🤖 Good sleep — higher focus today. Schedule deep work now.
Monday
April 26
categories
interactions
● tap to complete
■ drag to reschedule
↕ resize from bottom
🎯 My Goals
track progress across all areas of your life
all
🤝 social
💪 health & wellness
👨👩👧 family
🌱 personal dev
💰 finances
🙏 spiritual
💼 career
🧠 My Brain
brain dump · notes · journal · anything that needs to get out of your head
get it out of your head. no structure needed. no judgment here.
' +
'
';
})
.catch(function(err){ showAuthError('forgotError', err.message); });
}
function signOut(){
if(_fbAuth) _fbAuth.signOut();
}
// ── FIRESTORE SYNC ──
function loadFromFirestore(uid){
if(!_fbDb) return;
showSyncBanner('☁️ Loading your data...');
_fbDb.collection('users').doc(uid).get()
.then(function(doc){
if(doc.exists){
var data = doc.data();
if(data.schedTasks && data.schedTasks.length > 1) schedTasks = data.schedTasks;
if(data.goalsData && data.goalsData.length > 0) goalsData = data.goalsData;
if(data.prioritySettings) prioritySettings = data.prioritySettings;
if(data.userProfile) userProfile = data.userProfile;
if(data.appSettings) appSettings = data.appSettings;
// Re-render with loaded data
applyDarkMode();
applyProfile();
renderGoals();
ensureReviewTask();
renderWidgetArea();
refreshMyView();
hideSyncBanner();
toast('☁️ Data loaded');
} else {
// New user — save demo data to Firestore
saveToFirestore();
hideSyncBanner();
}
})
.catch(function(err){
console.warn('[Sphere] Firestore load failed:', err);
hideSyncBanner();
});
}
function saveToFirestore(){
if(!_fbDb || !_fbUser) return;
clearTimeout(_syncTimer);
_syncTimer = setTimeout(function(){
var data = {
schedTasks: schedTasks,
goalsData: goalsData,
prioritySettings: prioritySettings,
userProfile: userProfile,
appSettings: appSettings,
savedAt: firebase.firestore.FieldValue.serverTimestamp()
};
_fbDb.collection('users').doc(_fbUser.uid).set(data)
.then(function(){
var lbl = document.getElementById('lastSavedLabel');
if(lbl) lbl.textContent = 'Last saved: just now ☁️';
})
.catch(function(err){ console.warn('[Sphere] Firestore save failed:', err); });
}, 1000); // debounce 1s
}
// Override scheduleSave to also sync to Firestore
var _origScheduleSave = scheduleSave;
scheduleSave = function(){
_origScheduleSave();
saveToFirestore();
};
function showSyncBanner(msg){
var banner = document.getElementById('syncBanner');
var msgEl = document.getElementById('syncBannerMsg');
if(banner && msgEl){ msgEl.textContent = msg; banner.style.display = 'flex'; }
}
function hideSyncBanner(){
setTimeout(function(){
var banner = document.getElementById('syncBanner');
if(banner) banner.style.display = 'none';
}, 1500);
}
// ══════════════════════════════════════════
// IN-APP FEEDBACK
// ══════════════════════════════════════════
var _feedbackRating = 0;
var _feedbackTags = [];
var FEEDBACK_EMAIL = 'Osborn.Joshua36@gmail.com';
// Init EmailJS (free tier — 200 emails/month)
// Sign up at emailjs.com and replace these IDs
var EMAILJS_SERVICE = 'service_nbeqhpb';
var EMAILJS_TEMPLATE = ''; // not needed — using mailto fallback
var EMAILJS_KEY = 'L3mGqDexA5tAOQvnp';
function openFeedback(){
_feedbackRating = 0;
_feedbackTags = [];
document.querySelectorAll('.feedback-star').forEach(function(s){ s.classList.remove('active'); });
document.querySelectorAll('.feedback-tag').forEach(function(t){ t.classList.remove('active'); });
var txt = document.getElementById('feedbackText');
if(txt) txt.value = '';
var overlay = document.getElementById('feedbackOverlay');
if(overlay) overlay.style.display = 'flex';
}
function closeFeedback(){
var overlay = document.getElementById('feedbackOverlay');
if(overlay) overlay.style.display = 'none';
}
function setFeedbackStar(n){
_feedbackRating = n;
document.querySelectorAll('.feedback-star').forEach(function(s, i){
s.classList.toggle('active', i < n);
});
}
function toggleFeedbackTag(el, tag){
el.classList.toggle('active');
var idx = _feedbackTags.indexOf(tag);
if(idx >= 0) _feedbackTags.splice(idx,1);
else _feedbackTags.push(tag);
}
function submitFeedback(){
var text = (document.getElementById('feedbackText') || {}).value || '';
if(!text.trim() && _feedbackRating === 0){
toast('Please add a rating or message before sending');
return;
}
var btn = document.getElementById('feedbackSubmitBtn');
if(btn){ btn.textContent = 'Sending...'; btn.disabled = true; }
var userName = userProfile.name || 'Anonymous';
var userEmail = (_fbUser && _fbUser.email) || 'Not signed in';
var stars = _feedbackRating > 0 ? '⭐'.repeat(_feedbackRating) + ' (' + _feedbackRating + '/5)' : 'Not rated';
var tags = _feedbackTags.length ? _feedbackTags.join(', ') : 'None';
var now = new Date().toLocaleString('en-US', { dateStyle:'medium', timeStyle:'short' });
// Save to Firestore (permanent record — readable at console.firebase.google.com)
if(_fbDb){
_fbDb.collection('feedback').add({
uid: _fbUser ? _fbUser.uid : 'anonymous',
name: userName,
email: userEmail,
rating: _feedbackRating,
tags: _feedbackTags,
message: text,
timestamp: firebase.firestore.FieldValue.serverTimestamp()
});
}
// Open pre-filled email in their mail app
var subject = encodeURIComponent('Sphere Feedback — ' + stars + (tags !== 'None' ? ' — ' + tags : ''));
var body = encodeURIComponent(
'From: ' + userName + ' (' + userEmail + ')\n' +
'Rating: ' + stars + '\n' +
(tags !== 'None' ? 'Tags: ' + tags + '\n' : '') +
'Date: ' + now + '\n\n' +
text
);
window.open('mailto:' + FEEDBACK_EMAIL + '?subject=' + subject + '&body=' + body);
closeFeedback();
if(btn){ btn.textContent = 'Send feedback'; btn.disabled = false; }
toast('💙 Thanks for the feedback! It goes directly to the founder.');
}
// ── INIT ──
// Boot Firebase when the page loads
initFirebase();
var convData={
jamie:{name:'Jamie Lambert',av:'av1',init:'JL',status:'partner · online now'},
mom:{name:'Mom',av:'av2',init:'MA',status:'family · online now'},
alex:{name:'Alex Kim',av:'av4',init:'AK',status:'friend · last seen 2d ago'},
family:{name:'Family',av:'av5',init:'👨👩👧',status:'group · 4 members'}
};
var replies={
jamie:['Got it! 🌿','Sounds good 😊','On it!','Perfect, will do!'],
mom:['Of course sweetheart 💙','No problem at all!','Let me know!'],
alex:['Yes! Saturday works!','Sounds great!','Let me check...'],
family:["We're in! 🎉",'Sunday works!','See you all then!']
};
var currentConv='jamie';
function openConv(name){
currentConv=name;
const d=convData[name];
document.getElementById('chatName').textContent=d.name;
document.getElementById('chatEncName').textContent=d.name.split(' ')[0];
document.querySelectorAll('#convList .conv-row, .circle-contacts .contact-row').forEach(r=>r.classList.remove('active'));
document.getElementById('msgInput').placeholder=`message ${d.name.split(' ')[0]}...`;
}
// ══════════════════════════════════════════
// MY CIRCLE — TASK DETECTION ENGINE
// ══════════════════════════════════════════
var _detectedTasks = [];
function detectTasksInMessage(text, senderName){
if(!text || !text.trim()) return [];
var found = [];
var lc = text.toLowerCase();
// ── 1. Grocery / item scan ──
var groceryRe = /\b(banana|apple|milk|egg|bread|butter|cheese|yogh?urt|juice|coffee|tea|sugar|flour|rice|pasta|diaper|nappie|formula|wipe|soap|shampoo|toothpaste|toilet paper|paper towel|detergent|oat milk|almond milk|soy milk|avocado|tomato|onion|potato|carrot|spinach|lettuce|chicken|beef|fish|salmon|tuna|orange|strawberr|blueberr|mango|grape|olive oil|cooking oil|cereal|crackers?|chips?|chocolate|ice cream|sparkling water|sparkling)\w*/gi;
var gm = text.match(groceryRe);
if(gm){
var unique = [], seen = {};
gm.forEach(function(m){ var k=m.toLowerCase(); if(!seen[k]){seen[k]=true;unique.push(m);} });
found.push({ id:'dt'+Date.now()+Math.random().toString(36).slice(2,5), name:'Pick up: '+unique.join(', '), cat:'home', checked:true, source:senderName||'message', items:unique });
}
// ── 2. X needs Y ──
var needsM = text.match(/\b([A-Z][a-z]+)\s+needs?\s+([^.!?\n]+)/);
if(needsM){
var who=needsM[1], what=needsM[2].replace(/\s*(please|pls|thanks?)\.?$/i,'').trim();
if(what && what.length>1 && !found.some(function(f){return f.name.toLowerCase().includes(what.toLowerCase().slice(0,6));})){
found.push({ id:'dt'+Date.now()+Math.random().toString(36).slice(2,5), name:'Get '+what+' for '+who, cat:'home', checked:true, source:senderName||'message' });
}
}
// ── 3. Don't forget / remember to ──
var remM = text.match(/don'?t forget\s+(?:to\s+)?(.+?)(?:[.!?]|$)/i) || text.match(/remember\s+(?:to\s+)?(.+?)(?:[.!?]|$)/i);
if(remM){
var item=remM[1].replace(/\s*(please|pls|thanks?)\.?$/i,'').trim();
if(item && item.length>2 && !found.some(function(f){return f.name.toLowerCase().includes(item.toLowerCase().slice(0,8));})){
found.push({ id:'dt'+Date.now()+Math.random().toString(36).slice(2,5), name:item.charAt(0).toUpperCase()+item.slice(1), cat:actionCat('do',item), checked:true, source:senderName||'message' });
}
}
// ── 4. Can you call/email/book/schedule/order X ──
var actM = text.match(/can you\s+(call|email|text|book|schedule|arrange|order|check|pick up|grab|get)\s+(.+?)(?:[.!?]|$)/i);
if(actM && !gm){
var verb=actM[1], obj=actM[2].replace(/\s*(please|pls|thanks?|no rush|if you can)\.?$/i,'').trim();
if(obj && obj.length>1){
found.push({ id:'dt'+Date.now()+Math.random().toString(36).slice(2,5), name:verb.charAt(0).toUpperCase()+verb.slice(1)+' '+obj, cat:actionCat(verb,obj), checked:true, source:senderName||'message' });
}
}
return found;
}
function actionCat(verb, obj){
var t = (verb+' '+obj).toLowerCase();
if(/call|email|text|message/.test(t)) return 'social';
if(/doctor|dentist|appointment|therapy|prescription|pharmacy/.test(t)) return 'health';
if(/pay|bill|invoice|bank|money/.test(t)) return 'finances';
return 'home';
}
function showTaskDetectBanner(tasks, senderName){
if(!tasks || tasks.length===0) return;
tasks.forEach(function(t){
var dup=_detectedTasks.some(function(d){return d.name.toLowerCase().slice(0,10)===t.name.toLowerCase().slice(0,10);});
if(!dup) _detectedTasks.push(t);
});
renderDetectBanner(senderName);
}
// ── SMART TASK MATCHING ──
function findSimilarTasks(detectedName){
var q = detectedName.toLowerCase();
var groceryKw = /\b(groceri|grocery|store|shop|pick up|buy|banana|milk|egg|bread|oat milk|produce|market|item)\b/i;
var errandKw = /\b(errand|pick up|drop off|return|post office|pharmacy|dry clean)\b/i;
var results = [];
function scoreText(text, weight){
var t = text.toLowerCase(), score = 0;
q.split(/\s+/).forEach(function(w){ if(w.length > 2 && t.includes(w)) score += weight * 2; });
if(groceryKw.test(q) && groceryKw.test(t)) score += weight * 5;
if(errandKw.test(q) && errandKw.test(t)) score += weight * 3;
if(q.length > 5 && t.includes(q.slice(0,6))) score += weight * 3;
return score;
}
schedTasks.forEach(function(t){
if(t._isReviewTask || t.done) return;
var s = scoreText(t.title, 1);
if(s > 0) results.push({type:'sched', item:t, score:s, reason:'Schedule task \u00b7 ' + minToTime(t.startMin)});
});
goalsData.forEach(function(g){
var s = scoreText(g.title, 1);
g.subtasks.forEach(function(sub){ s += scoreText(sub.name, 0.4); });
if(s > 0) results.push({type:'goal', item:g, score:s, reason:'Goal \u00b7 ' + g.subtasks.length + ' existing steps'});
});
return results.sort(function(a,b){ return b.score - a.score; });
}
// ── RENDER DETECT BANNER ──
// ══════════════════════════════════════════
// MY CIRCLE — TASK DETECTION FLOW
// ══════════════════════════════════════════
// ── RENDER DETECT BANNER ──
// Shows each detected item with a guided Yes/No decision then task vs subtask choice
function renderDetectBanner(senderName){
var banner = document.getElementById('taskDetectBanner');
var list = document.getElementById('detectedTaskList');
var sub = document.getElementById('taskDetectSub');
if(!banner || !list) return;
if(_detectedTasks.length === 0){ banner.style.display = 'none'; return; }
if(sub) sub.textContent = _detectedTasks.length + ' possible task' +
(_detectedTasks.length > 1 ? 's' : '') + ' spotted' +
(senderName ? ' from ' + senderName : '');
list.innerHTML = '';
_detectedTasks.forEach(function(task){
var row = document.createElement('div');
row.className = 'detected-task-item';
row.style.cssText = 'flex-direction:column;align-items:stretch;gap:0;padding:10px 12px;';
// ── Item name ──
var nameRow = document.createElement('div');
nameRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px;';
var bullet = document.createElement('div');
bullet.style.cssText = 'width:7px;height:7px;border-radius:50%;background:var(--plum);flex-shrink:0;';
var nm = document.createElement('div');
nm.style.cssText = 'font-size:12px;font-weight:500;color:var(--txt);flex:1;';
nm.textContent = task.name;
var rm = document.createElement('button');
rm.className = 'dt-remove'; rm.textContent = '\u00d7'; rm.title = 'ignore';
rm.onclick = (function(tid){ return function(){
_detectedTasks = _detectedTasks.filter(function(t){ return t.id !== tid; });
renderDetectBanner();
}; })(task.id);
nameRow.appendChild(bullet); nameRow.appendChild(nm); nameRow.appendChild(rm);
row.appendChild(nameRow);
// ── Step 1: Will you action this? ──
if(!task._decision){
var q1 = document.createElement('div');
q1.style.cssText = 'font-size:10px;color:var(--hint);margin-bottom:6px;margin-left:15px;';
q1.textContent = 'Will you action this?';
row.appendChild(q1);
var btnRow = document.createElement('div');
btnRow.style.cssText = 'display:flex;gap:6px;margin-left:15px;';
var yesBtn = makeDecisionBtn('\u2714 yes, I\'ll do it', 'var(--plum)', '#fff');
yesBtn.onclick = function(){
task._decision = 'yes';
renderDetectBanner(senderName);
};
var noBtn = makeDecisionBtn('\u2715 not right now', 'var(--cream-mid)', 'var(--muted)');
noBtn.onclick = function(){
_detectedTasks = _detectedTasks.filter(function(t){ return t.id !== task.id; });
renderDetectBanner(senderName);
};
btnRow.appendChild(yesBtn); btnRow.appendChild(noBtn);
row.appendChild(btnRow);
// ── Step 2: Task or Subtask? ──
} else if(task._decision === 'yes' && !task._taskType){
// Show smart match suggestion if one exists
var matches = findSimilarTasks(task.name);
var best = matches.length > 0 ? matches[0] : null;
if(best){
var suggestion = document.createElement('div');
suggestion.style.cssText = 'background:rgba(91,170,126,.1);border:1px solid rgba(91,170,126,.25);border-radius:8px;padding:7px 10px;margin-left:15px;margin-bottom:7px;display:flex;align-items:center;gap:8px;';
var suggIcon = document.createElement('div'); suggIcon.textContent = '\uD83D\uDD17'; suggIcon.style.fontSize='13px';
var suggText = document.createElement('div'); suggText.style.cssText='flex:1;font-size:10px;color:var(--sage-d);line-height:1.5;';
suggText.innerHTML = 'Suggested: add as item under ' + escHtml(best.item.title) + '';
if(best.type === 'sched' && best.item.subtasks){
suggText.innerHTML += '📧
' +
'Check your email
' +
'We sent a password reset link to ' + email + '
' +
'' + best.item.subtasks.length + ' item' + (best.item.subtasks.length!==1?'s':'') + ' already listed'; } var suggBtn = document.createElement('button'); suggBtn.style.cssText = 'background:var(--sage);color:#fff;border:none;border-radius:7px;padding:4px 10px;font-size:10px;font-weight:600;cursor:pointer;font-family:\'DM Sans\',sans-serif;white-space:nowrap;'; suggBtn.textContent = 'use this'; suggBtn.onclick = (function(t, m){ return function(){ addItemToTask(t, m); }; })(task, best); suggestion.appendChild(suggIcon); suggestion.appendChild(suggText); suggestion.appendChild(suggBtn); row.appendChild(suggestion); } var q2 = document.createElement('div'); q2.style.cssText = 'font-size:10px;color:var(--hint);margin-bottom:6px;margin-left:15px;'; q2.textContent = best ? 'Or choose another option:' : 'How do you want to add this?'; row.appendChild(q2); var btnRow2 = document.createElement('div'); btnRow2.style.cssText = 'display:flex;gap:6px;margin-left:15px;flex-wrap:wrap;'; var subBtn = makeDecisionBtn('\uFF0B subtask of existing', 'var(--sky-l)', 'var(--sky-d)'); subBtn.onclick = function(){ task._taskType = 'subtask'; renderDetectBanner(senderName); }; var newBtn = makeDecisionBtn('\u25A1 new task', 'var(--navy)', '#fff'); newBtn.onclick = function(){ task._taskType = 'new'; renderDetectBanner(senderName); }; btnRow2.appendChild(subBtn); btnRow2.appendChild(newBtn); row.appendChild(btnRow2); // ── Step 3a: Subtask — pick which task to attach to ── } else if(task._taskType === 'subtask'){ var matches2 = findSimilarTasks(task.name); var q3 = document.createElement('div'); q3.style.cssText = 'font-size:10px;color:var(--hint);margin-bottom:6px;margin-left:15px;'; q3.textContent = 'Add as a subtask of:'; row.appendChild(q3); var matchList = document.createElement('div'); matchList.style.cssText = 'display:flex;flex-direction:column;gap:4px;margin-left:15px;'; // Show top matches var shown = matches2.slice(0, 4); shown.forEach(function(match){ var mRow = document.createElement('div'); mRow.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 9px;border:1.5px solid var(--bdr);border-radius:9px;cursor:pointer;transition:all .14s;background:var(--cream-lt);'; mRow.onmouseenter = function(){ mRow.style.borderColor='var(--navy)'; mRow.style.background='#F0F4FF'; }; mRow.onmouseleave = function(){ mRow.style.borderColor='var(--bdr)'; mRow.style.background='var(--cream-lt)'; }; var mIcon = document.createElement('div'); if(match.type==='sched'){ var c=CAT_COLORS[match.item.cat]||CAT_COLORS.other; mIcon.textContent=c.icon; } else { mIcon.textContent=match.item.icon||'\uD83C\uDFAF'; } mIcon.style.fontSize='14px'; var mName = document.createElement('div'); mName.style.cssText='flex:1;font-size:11px;font-weight:500;color:var(--txt);'; mName.textContent = match.item.title; var mMeta = document.createElement('div'); mMeta.style.cssText='font-size:9px;color:var(--hint);'; mMeta.textContent = match.type==='sched' ? minToTime(match.item.startMin) : 'goal'; mRow.appendChild(mIcon); mRow.appendChild(mName); mRow.appendChild(mMeta); mRow.onclick = (function(m){ return function(){ addItemToTask(task, m); }; })(match); matchList.appendChild(mRow); }); // "other task" option if no matches or user wants to pick var otherRow = document.createElement('div'); otherRow.style.cssText='display:flex;align-items:center;gap:8px;padding:6px 9px;border:1.5px dashed var(--bdr-md);border-radius:9px;cursor:pointer;'; otherRow.innerHTML='
+
choose a different task\u2026
';
otherRow.onclick = (function(t){ return function(){ openTaskMatchDrawer(t); }; })(task);
matchList.appendChild(otherRow);
row.appendChild(matchList);
// ── Step 3b: New task — type selector ──
} else if(task._taskType === 'new'){
var q4 = document.createElement('div');
q4.style.cssText = 'font-size:10px;color:var(--hint);margin-bottom:6px;margin-left:15px;';
q4.textContent = 'What kind of task is it?';
row.appendChild(q4);
var typeRow = document.createElement('div');
typeRow.style.cssText='display:flex;gap:6px;margin-left:15px;flex-wrap:wrap;';
var types = [
{id:'recurring', label:'\u21BB recurring', desc:'Repeats on a schedule'},
{id:'timebound', label:'\u23F0 deadline', desc:'Has a due date'},
{id:'adhoc', label:'\u2736 ad hoc', desc:'One-off, flexible timing'},
];
types.forEach(function(tp){
var tBtn = makeDecisionBtn(tp.label, tp.id==='adhoc'?'var(--cream-mid)':tp.id==='timebound'?'var(--rose-l)':'var(--sage-l)',
tp.id==='adhoc'?'var(--muted)':tp.id==='timebound'?'var(--rose-d)':'var(--sage-d)');
tBtn.title = tp.desc;
tBtn.onclick = (function(tpId){ return function(){
createNewTaskFromDetected(task, tpId);
}; })(tp.id);
typeRow.appendChild(tBtn);
});
var backBtn = makeDecisionBtn('\u2190 back', 'transparent', 'var(--hint)');
backBtn.style.borderColor='var(--bdr)';
backBtn.onclick = function(){ task._taskType = null; renderDetectBanner(senderName); };
typeRow.appendChild(backBtn);
row.appendChild(typeRow);
}
list.appendChild(row);
});
banner.style.display = 'block';
}
// Small helper for consistent decision button styling
function makeDecisionBtn(label, bg, color){
var btn = document.createElement('button');
btn.style.cssText='padding:5px 12px;border-radius:20px;border:1.5px solid transparent;background:'+bg+';color:'+color+';font-size:10px;font-weight:600;cursor:pointer;font-family:\'DM Sans\',sans-serif;white-space:nowrap;transition:all .14s;';
btn.textContent = label;
btn.onmouseenter = function(){ btn.style.opacity='.8'; };
btn.onmouseleave = function(){ btn.style.opacity='1'; };
return btn;
}
// ── CREATE NEW TASK FROM DETECTED ITEM ──
function createNewTaskFromDetected(detectedTask, taskType){
var nowMin = new Date().getHours()*60 + new Date().getMinutes();
var cursor = snapToGrid(nowMin + 30);
while(hasConflict(cursor, 30, null) && cursor < DAY_END*60) cursor += 5;
var items = detectedTask.items && detectedTask.items.length > 0
? detectedTask.items : [detectedTask.name];
var newTask = {
id: ++nextTaskId,
title: detectedTask.name,
startMin: cursor,
durMin: 30,
cat: 'home',
done: false,
recurring: taskType === 'recurring',
recurType: taskType === 'recurring' ? 'daily' : '',
missed: false,
taskType: taskType,
deadline: null,
subtasks: [],
};
// If there are sub-items (e.g. bananas, milk from a grocery detection), add them as subtasks
if(items.length > 1){
items.forEach(function(item){
newTask.subtasks.push({
id: 's'+nextSubId++, name: item, done: false,
taskType:'adhoc', recurFreq:'', deadline:null
});
});
}
schedTasks.push(newTask);
_detectedTasks = _detectedTasks.filter(function(t){ return t.id !== detectedTask.id; });
renderDetectBanner();
renderTimeline();
var typeLabel = {recurring:'Recurring',timebound:'Time-bound',adhoc:'Ad hoc'}[taskType] || '';
toast('\uD83D\uDCC5 ' + typeLabel + ' task \u201c' + detectedTask.name + '\u201d added at ' + minToTime(cursor));
}
// ── ADD ITEM TO EXISTING TASK AS SUBTASK ──
function addItemToTask(detectedTask, match){
var items = detectedTask.items && detectedTask.items.length > 0
? detectedTask.items : [detectedTask.name];
var added = 0;
if(match.type === 'sched'){
var t = match.item;
if(!t.subtasks) t.subtasks = [];
items.forEach(function(item){
if(!t.subtasks.some(function(s){ return s.name.toLowerCase()===item.toLowerCase(); })){
t.subtasks.push({id:'s'+nextSubId++, name:item, done:false, taskType:'adhoc', recurFreq:'', deadline:null});
added++;
}
});
_detectedTasks = _detectedTasks.filter(function(d){ return d.id !== detectedTask.id; });
renderDetectBanner(); renderTimeline();
toast('\u2705 ' + items.length + ' item' + (items.length>1?'s':'') + ' added to \u201c' + t.title + '\u201d');
} else {
var g = match.item;
items.forEach(function(item){
if(!g.subtasks.some(function(s){ return s.name.toLowerCase()===item.toLowerCase(); })){
g.subtasks.push({id:'s'+nextSubId++, name:item, done:false, taskType:'adhoc', recurFreq:'', deadline:null, meta:'from My Circle'});
added++;
}
});
_detectedTasks = _detectedTasks.filter(function(d){ return d.id !== detectedTask.id; });
renderDetectBanner(); renderGoals();
toast('\u2705 ' + items.length + ' item' + (items.length>1?'s':'') + ' added to \u201c' + g.title + '\u201d');
}
}
// ── TASK MATCH DRAWER (full chooser when user wants to browse all options) ──
function openTaskMatchDrawer(detectedTask){
var ex=document.getElementById('taskMatchDrawer'); if(ex) ex.remove();
var es=document.getElementById('taskMatchScrim'); if(es) es.remove();
var scrim=document.createElement('div'); scrim.id='taskMatchScrim';
scrim.style.cssText='position:fixed;inset:0;background:rgba(30,50,69,.45);z-index:810;';
scrim.onclick=closeTaskMatchDrawer; document.body.appendChild(scrim);
var wrap=document.createElement('div'); wrap.id='taskMatchDrawer';
wrap.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:811;display:flex;justify-content:center;';
document.body.appendChild(wrap);
var drawer=document.createElement('div');
drawer.style.cssText='background:#fff;border-radius:20px 20px 0 0;box-shadow:0 -8px 40px rgba(30,50,69,.22);padding:20px 22px 36px;width:100%;max-width:540px;max-height:80vh;overflow-y:auto;animation:slideUp .22s ease;';
wrap.appendChild(drawer);
var handle=document.createElement('div'); handle.style.cssText='width:36px;height:4px;border-radius:2px;background:#d0dae6;margin:0 auto 16px;'; drawer.appendChild(handle);
var closeBtn=document.createElement('button'); closeBtn.textContent='\u00d7'; closeBtn.style.cssText='position:absolute;top:16px;right:18px;width:30px;height:30px;border-radius:50%;border:none;background:#eef3f8;color:#4a6580;font-size:18px;cursor:pointer;'; closeBtn.onclick=closeTaskMatchDrawer; drawer.appendChild(closeBtn);
var hdr=document.createElement('div');
hdr.innerHTML='Add as subtask of\u2026
'+
'Item: '+escHtml(detectedTask.name)+'
';
drawer.appendChild(hdr);
// Schedule tasks
var schedTitle=document.createElement('div'); schedTitle.style.cssText='font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--hint);margin-bottom:8px;'; schedTitle.textContent='My Schedule tasks'; drawer.appendChild(schedTitle);
schedTasks.filter(function(t){return !t._isReviewTask&&!t.done;}).forEach(function(t){
var colors=CAT_COLORS[t.cat]||CAT_COLORS.other;
drawer.appendChild(makeMatchCard(colors.icon, colors.bg, t.title,
minToTime(t.startMin) + (t.subtasks&&t.subtasks.length?' \u00b7 '+t.subtasks.length+' items':''),
function(){ closeTaskMatchDrawer(); addItemToTask(detectedTask,{type:'sched',item:t}); }));
});
// Goals
var goalTitle=document.createElement('div'); goalTitle.style.cssText='font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--hint);margin:12px 0 8px;'; goalTitle.textContent='My Goals'; drawer.appendChild(goalTitle);
goalsData.forEach(function(g){
drawer.appendChild(makeMatchCard(g.icon||'\uD83C\uDFAF', g.iconBg||'var(--cream-mid)', g.title,
'goal \u00b7 '+g.subtasks.length+' steps',
function(){ closeTaskMatchDrawer(); addItemToTask(detectedTask,{type:'goal',item:g}); }));
});
}
function makeMatchCard(icon, iconBg, title, meta, onclick){
var card=document.createElement('div');
card.style.cssText='display:flex;align-items:center;gap:11px;padding:10px 12px;border:1.5px solid var(--bdr);border-radius:11px;cursor:pointer;margin-bottom:6px;transition:all .14s;background:var(--cream-lt);';
card.onmouseenter=function(){card.style.borderColor='var(--navy)';card.style.background='#F0F4FF';};
card.onmouseleave=function(){card.style.borderColor='var(--bdr)';card.style.background='var(--cream-lt)';};
var ic=document.createElement('div'); ic.style.cssText='width:34px;height:34px;border-radius:8px;background:'+iconBg+';display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;'; ic.textContent=icon;
var body=document.createElement('div'); body.style.flex='1';
var nm=document.createElement('div'); nm.style.cssText='font-size:12px;font-weight:500;color:var(--txt);'; nm.textContent=title;
var mt=document.createElement('div'); mt.style.cssText='font-size:10px;color:var(--hint);margin-top:1px;'; mt.textContent=meta;
body.appendChild(nm); body.appendChild(mt);
var arr=document.createElement('div'); arr.style.cssText='font-size:16px;color:var(--hint);'; arr.textContent='\u203A';
card.appendChild(ic); card.appendChild(body); card.appendChild(arr);
card.onclick=onclick; return card;
}
function closeTaskMatchDrawer(){
var d=document.getElementById('taskMatchDrawer'); if(d) d.remove();
var s=document.getElementById('taskMatchScrim'); if(s) s.remove();
}
function dismissTaskDetect(){
_detectedTasks=[];
var b=document.getElementById('taskDetectBanner'); if(b) b.style.display='none';
}
function addAllDetectedTasks(){
// Open the full match drawer for the first uncategorised item
var first = _detectedTasks.find(function(t){ return !t._decision; }) || _detectedTasks[0];
if(!first){ toast('No items to action'); return; }
// Start the guided flow by marking as yes
first._decision = 'yes';
renderDetectBanner();
}
function addDetectedToSchedule(){
// Quick-add all checked items to schedule as adhoc tasks
var toAdd = _detectedTasks.filter(function(t){ return t.checked; });
if(toAdd.length===0){ toast('Select at least one item first'); return; }
var nowMin = new Date().getHours()*60+new Date().getMinutes();
var cursor = snapToGrid(nowMin+30);
toAdd.forEach(function(t){
while(hasConflict(cursor,30,null)&&cursor${moodEmoji}
${today}
mood: ${selectedJournalMood||'not set'} · new entry
new ✦
Focus session
' +
'Focusing on: ' + escHtml(taskName) + '
';
drawer.appendChild(hdr);
// ── Timer ring ──
var ringWrap = document.createElement('div');
ringWrap.className = 'focus-timer-ring';
var circ = 2 * Math.PI * 62;
ringWrap.innerHTML =
'' +
'' + _focusSettings.duration + ':00
' +
'minutes remaining
';
drawer.appendChild(ringWrap);
// ── Duration picker ──
var durRow = document.createElement('div');
durRow.className = 'focus-duration-row';
[15, 25, 45, 60].forEach(function(mins){
var btn = document.createElement('button');
btn.className = 'focus-dur-btn' + (mins === _focusSettings.duration ? ' active' : '');
btn.textContent = mins + ' min';
btn.onclick = function(){
_focusSettings.duration = mins;
durRow.querySelectorAll('.focus-dur-btn').forEach(function(b){ b.classList.remove('active'); });
btn.classList.add('active');
document.getElementById('ftLabel').textContent = mins + ':00';
};
durRow.appendChild(btn);
});
drawer.appendChild(durRow);
// ── Block Interruptions section ──
var blockSec = document.createElement('div');
blockSec.className = 'focus-block-section';
var blockTitle = document.createElement('div');
blockTitle.className = 'focus-block-title';
blockTitle.textContent = '🛡 block interruptions';
blockSec.appendChild(blockTitle);
function makeToggleRow(icon, iconBg, name, sub, settingKey){
var row = document.createElement('div'); row.className = 'focus-block-row';
var ic = document.createElement('div'); ic.className = 'focus-block-icon';
ic.style.background = iconBg; ic.textContent = icon;
var lbl = document.createElement('div'); lbl.className = 'focus-block-label';
lbl.innerHTML = '' + name + '
' +
'' + sub + '
';
var tog = document.createElement('button'); tog.className = 'focus-toggle' + (_focusSettings[settingKey] ? ' on' : '');
tog.onclick = function(){
_focusSettings[settingKey] = !_focusSettings[settingKey];
tog.classList.toggle('on', _focusSettings[settingKey]);
};
row.appendChild(ic); row.appendChild(lbl); row.appendChild(tog);
return row;
}
blockSec.appendChild(makeToggleRow('🔕', 'var(--navy)', 'Do Not Disturb',
'Silence notifications for the full session', 'dnd'));
blockSec.appendChild(makeToggleRow('📱', 'var(--plum-l)', 'Allow urgent messages only',
'Emergency contacts & calls still come through', 'allowUrgent'));
// App blocking chips
var appRow = document.createElement('div'); appRow.className = 'focus-block-row';
appRow.style.flexDirection = 'column';
appRow.style.alignItems = 'flex-start';
var appLbl = document.createElement('div'); appLbl.className = 'focus-block-label';
appLbl.innerHTML = '🚫 Block distracting apps
' +
'Tap to toggle — blocked apps show an interstitial during focus
';
appRow.appendChild(appLbl);
var allApps = ['Facebook','Instagram','Twitter / X','TikTok','YouTube','Reddit','Snapchat','News apps'];
var chips = document.createElement('div'); chips.className = 'focus-app-chips';
allApps.forEach(function(app){
var chip = document.createElement('div');
var isBlocked = _focusSettings.blockedApps.indexOf(app) >= 0;
chip.className = 'focus-app-chip' + (isBlocked ? ' blocked' : '');
chip.innerHTML = '+× ' + app;
chip.onclick = function(){
var idx = _focusSettings.blockedApps.indexOf(app);
if(idx >= 0){ _focusSettings.blockedApps.splice(idx,1); chip.classList.remove('blocked'); }
else { _focusSettings.blockedApps.push(app); chip.classList.add('blocked'); }
};
chips.appendChild(chip);
});
appRow.appendChild(chips);
blockSec.appendChild(appRow);
// Note about OS-level controls
var osNote = document.createElement('div');
osNote.style.cssText = 'font-size:10px;color:var(--hint);margin-top:10px;line-height:1.6;padding:8px 10px;background:rgba(30,50,69,.04);border-radius:8px;';
osNote.innerHTML = 'Note: Do Not Disturb and app restrictions require device permission. ' +
'On iOS, go to Settings → Focus. On Android, go to Settings → Digital Wellbeing. ' +
'Sphere will remind you to enable these before starting.';
blockSec.appendChild(osNote);
drawer.appendChild(blockSec);
// ── Start button ──
var startBtn = document.createElement('button');
startBtn.style.cssText = 'width:100%;background:var(--navy);color:#fff;border:none;border-radius:12px;padding:14px;font-size:14px;font-weight:600;font-family:\'DM Sans\',sans-serif;cursor:pointer;transition:background .15s;';
startBtn.textContent = '▶ start focus session';
startBtn.onmouseenter = function(){ startBtn.style.background = 'var(--terra)'; };
startBtn.onmouseleave = function(){ startBtn.style.background = 'var(--navy)'; };
startBtn.onclick = function(){
closeFocusDrawer();
startFocusMode();
};
drawer.appendChild(startBtn);
}
function closeFocusDrawer(){
var d = document.getElementById('focusSessionDrawer'); if(d) d.remove();
var s = document.getElementById('focusSessionScrim'); if(s) s.remove();
}
// ── ACTIVE FOCUS MODE OVERLAY ──
function startFocusMode(){
var totalSecs = _focusSettings.duration * 60;
_focusSecondsLeft = totalSecs;
_focusRunning = true;
// Build full-screen focus overlay
var overlay = document.createElement('div');
overlay.id = 'focusActiveOverlay';
overlay.className = 'focus-active-overlay';
document.body.appendChild(overlay);
// Task name
var taskName = (prioritySettings.topOne && prioritySettings.topOne.title) || 'deep work';
var taskEl = document.createElement('div');
taskEl.style.cssText = 'font-size:13px;color:rgba(255,255,255,.4);margin-bottom:12px;letter-spacing:.04em;text-transform:uppercase;';
taskEl.textContent = '🎯 focusing on';
overlay.appendChild(taskEl);
var taskNameEl = document.createElement('div');
taskNameEl.style.cssText = 'font-family:\'Fraunces\',serif;font-size:22px;color:rgba(255,255,255,.85);margin-bottom:36px;max-width:340px;text-align:center;line-height:1.3;';
taskNameEl.textContent = taskName;
overlay.appendChild(taskNameEl);
// Big timer
var timerEl = document.createElement('div');
timerEl.className = 'focus-active-timer';
timerEl.id = 'focusActiveTimer';
timerEl.textContent = formatFocusTime(_focusSecondsLeft);
overlay.appendChild(timerEl);
var subEl = document.createElement('div');
subEl.style.cssText = 'font-size:12px;color:rgba(255,255,255,.3);margin-bottom:36px;';
subEl.textContent = 'stay with it — you\'ve got this';
overlay.appendChild(subEl);
// Blocked apps badges
if(_focusSettings.blockedApps.length > 0){
var blockedWrap = document.createElement('div');
blockedWrap.className = 'focus-active-blocked';
_focusSettings.blockedApps.forEach(function(app){
var badge = document.createElement('div');
badge.className = 'focus-blocked-badge';
badge.textContent = '🚫 ' + app;
blockedWrap.appendChild(badge);
});
overlay.appendChild(blockedWrap);
}
// DND notice
if(_focusSettings.dnd){
var dndEl = document.createElement('div');
dndEl.className = 'focus-dnd-notice';
dndEl.innerHTML = '🔕 Do Not Disturb is on.' + (_focusSettings.allowUrgent ? 'Emergency contacts can still reach you.' : 'All notifications are silenced.') + '
To activate at the OS level: pull down your notification shade and enable DND.'; overlay.appendChild(dndEl); } // Action buttons var actions = document.createElement('div'); actions.className = 'focus-active-actions'; actions.style.marginTop = '36px'; var pauseBtn = document.createElement('button'); pauseBtn.className = 'focus-end-btn'; pauseBtn.id = 'focusPauseBtn'; pauseBtn.textContent = '⏸ pause'; pauseBtn.onclick = function(){ _focusRunning = !_focusRunning; pauseBtn.textContent = _focusRunning ? '⏸ pause' : '▶ resume'; }; var endBtn = document.createElement('button'); endBtn.className = 'focus-end-btn'; endBtn.textContent = '⏹ end session'; endBtn.onclick = function(){ endFocusMode(false); }; var doneBtn = document.createElement('button'); doneBtn.className = 'focus-end-btn primary'; doneBtn.textContent = '✓ task complete'; doneBtn.onclick = function(){ // Mark the top-1 task done if it exists if(prioritySettings.topOne){ var t = schedTasks.find(function(t){ return t.id === prioritySettings.topOne.id; }); if(t){ var prev=t.done; t.done=true; t.missed=false; toastUndo('✅ '+t.title+' — done!',t,prev); } } endFocusMode(true); }; actions.appendChild(pauseBtn); actions.appendChild(endBtn); actions.appendChild(doneBtn); overlay.appendChild(actions); // Start countdown var circ = 2 * Math.PI * 62; clearInterval(_focusTimer); _focusTimer = setInterval(function(){ if(!_focusRunning) return; _focusSecondsLeft--; var timerDisplay = document.getElementById('focusActiveTimer'); if(timerDisplay) timerDisplay.textContent = formatFocusTime(_focusSecondsLeft); if(_focusSecondsLeft <= 0){ clearInterval(_focusTimer); endFocusMode(true); } }, 1000); // Show DND reminder toast if(_focusSettings.dnd){ setTimeout(function(){ toast('🔕 Remember to enable Do Not Disturb on your device'); }, 800); } } function endFocusMode(completed){ clearInterval(_focusTimer); _focusRunning = false; var overlay = document.getElementById('focusActiveOverlay'); if(overlay) overlay.remove(); if(completed){ toast('🎉 Focus session complete — great work!'); renderWidgetArea(); } else { var mins = Math.round((_focusSettings.duration * 60 - _focusSecondsLeft) / 60); toast('Session ended — ' + mins + ' min focused. Every minute counts. 🌿'); } } function formatFocusTime(secs){ var m = Math.floor(secs / 60), s = secs % 60; return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; } // ── MODALS ── function openModal(type){ document.querySelectorAll('[id^="modal-"]').forEach(m=>m.style.display='none'); const m=document.getElementById('modal-'+type); if(m){ m.style.display='block'; document.getElementById('modalWrap').classList.add('open'); } } function closeModal(){ document.getElementById('modalWrap').classList.remove('open'); } document.getElementById('modalWrap').addEventListener('click',e=>{ if(e.target===document.getElementById('modalWrap'))closeModal(); }); // ── LOCK ── function lockApp(){ toast('App locked 🔒'); } // ── TOAST ── var toastT = null; var _lastCompleted = null; // {task, prevDone} for undo function toast(msg){ var t = document.getElementById('toast'); var m = document.getElementById('toastMsg'); var u = document.getElementById('toastUndoBtn'); if(!t || !m) return; m.textContent = msg; if(u) u.style.display = 'none'; t.style.display = 'flex'; clearTimeout(toastT); toastT = setTimeout(function(){ t.style.display = 'none'; }, 3000); } function toastUndo(msg, task, prevDone){ _lastCompleted = { task: task, prevDone: prevDone }; scheduleSave(); // save completion state var t = document.getElementById('toast'); var m = document.getElementById('toastMsg'); var u = document.getElementById('toastUndoBtn'); if(!t || !m) return; m.textContent = msg; if(u) u.style.display = 'inline-block'; t.style.display = 'flex'; clearTimeout(toastT); toastT = setTimeout(function(){ t.style.display = 'none'; _lastCompleted = null; }, 5000); } function undoLastComplete(){ if(!_lastCompleted) return; _lastCompleted.task.done = _lastCompleted.prevDone; _lastCompleted.task.missed = false; _lastCompleted = null; clearTimeout(toastT); var t = document.getElementById('toast'); if(t) t.style.display = 'none'; renderTimeline(); toast('↩ undone'); }