// The 5 weekly priorities — heart of the app.
// Escalation system: rollovers field drives stage (fresh → rolled → final).
// fresh = rollovers 0 — normal state
// rolled = rollovers 1 — moved from last week, amber/red nudges
// final = rollovers 2+ OR extended:true — locked, no more moves
// WeekPressure banner shows urgency based on day of week.
// ── Stage + week-phase helpers ─────────────────────────────────────────────
function getPriorityStage(p) {
if (p.extended || (p.rollovers || 0) >= 2) return 'final';
if ((p.rollovers || 0) === 1) return 'rolled';
return 'fresh';
}
function getWeekPhase() {
// 0=Sun 1=Mon 2=Tue 3=Wed 4=Thu 5=Fri 6=Sat
const d = new Date().getDay();
if (d === 0) return 'closing';
if (d >= 5) return 'urgent';
if (d === 4) return 'warming';
return 'calm';
}
function isWeekEnd() {
const d = new Date().getDay();
return d === 5 || d === 6; // Fri or Sat
}
// ── WeekPressure banner ────────────────────────────────────────────────────
function WeekPressure({ priorities }) {
const phase = getWeekPhase();
const DAY_LABELS = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
// Map JS getDay() (Sun=0) → ribbon index (Mon=0 … Sun=6)
const dayJs = new Date().getDay();
const todayIdx = dayJs === 0 ? 6 : dayJs - 1;
const rolled = priorities.filter(p => getPriorityStage(p) === 'rolled').length;
const finals = priorities.filter(p => getPriorityStage(p) === 'final').length;
const atRisk = priorities.filter(p => getPriorityStage(p) !== 'final').length;
const phaseLabel = {
calm: '',
warming: 'THU · WARMING UP',
urgent: dayJs === 6 ? 'SAT · LAST CALL' : 'FRI · LAST CHANCE',
closing: 'SUN · CLOSING TIME',
}[phase];
const showAlert = phase !== 'calm' && atRisk > 0;
return (
{showAlert && (
{phaseLabel}
{atRisk} {atRisk === 1 ? 'priority' : 'priorities'} roll{atRisk === 1 ? 's' : ''} over if not closed this weekend.
{rolled > 0 && ` ${rolled} already on borrowed time — finish ${rolled === 1 ? 'it' : 'them'}.`}
)}
{DAY_LABELS.map((d, i) => (
{d}
))}
THE LINE ↑
);
}
// ── Main Priorities section ────────────────────────────────────────────────
function Priorities() {
const CAP = 5;
const { state, toggleTask, addTask, addPriority, renamePriority, transferPriority,
deletePriority, completePriority, startBurst, startWrap, setPriorityCategory,
grantExtension } = useStore();
const handleToggleTask = (priorityId, task) => {
if (!task.done) {
toggleTask(priorityId, task.id);
startWrap(priorityId, task.id);
} else {
toggleTask(priorityId, task.id);
}
};
const usedCount = state.priorities.length;
const pips = Array.from({ length: CAP }, (_, i) => i < usedCount);
const covered = new Set(state.priorities.map((p) => p.tag));
return (
This week
Five in the running, max. Complete one to free up a slot — move one to next week, once.
{pips.map((on, i) => )}
{usedCount}/{CAP}
{state.priorities.slice(0, CAP).map((p, idx) => (
handleToggleTask(p.id, task)}
onAddTask={(label) => addTask(p.id, label)}
onRename={(t) => renamePriority(p.id, t)}
onSetCategory={(tag) => setPriorityCategory(p.id, tag)}
onTransfer={() => transferPriority(p.id)}
onExtend={() => grantExtension(p.id)}
onDelete={() => deletePriority(p.id)}
onComplete={() => completePriority(p.id)}
onBurst={(tid) => startBurst(p.id, tid, state.burst.length)}
/>
))}
{Array.from({ length: Math.max(0, CAP - usedCount) }, (_, i) => (
))}
);
}
// ── Coverage strip ─────────────────────────────────────────────────────────
function CoverageStrip({ covered }) {
const missing = CATEGORY_ORDER.filter((k) => !covered.has(k));
return (
Coverage
{CATEGORY_ORDER.map((k) => {
const c = CATEGORIES[k];
const on = covered.has(k);
return (
{c.label}
);
})}
{missing.length > 0 && (
{missing.length === 5
? 'Pick at least one of each for breadth'
: missing.length === 1
? `Missing: ${CATEGORIES[missing[0]].label}`
: `${missing.length} categories uncovered`}
)}
);
}
// ── Priority card ──────────────────────────────────────────────────────────
function PriorityCard({ p, idx, onToggleTask, onAddTask, onRename, onSetCategory,
onTransfer, onExtend, onDelete, onComplete, onBurst }) {
const done = p.tasks.filter((t) => t.done).length;
const pct = p.tasks.length ? Math.round(done / p.tasks.length * 100) : 0;
const [adding, setAdding] = React.useState(false);
const [draft, setDraft] = React.useState('');
const [catOpen, setCatOpen] = React.useState(false);
const [extConfirm, setExtConfirm] = React.useState(false);
const titleRef = React.useRef(null);
const stage = getPriorityStage(p);
const weekend = isWeekEnd();
const canTransfer = stage === 'fresh' && !p.transferred;
const lastChance = stage === 'fresh' && weekend;
const commitTitle = () => {
const v = titleRef.current?.textContent?.trim();
if (v && v !== p.title) onRename(v);
};
const submitTask = (e) => {
e?.preventDefault();
if (draft.trim()) onAddTask(draft.trim());
setDraft(''); setAdding(false);
};
const cat = CATEGORIES[p.tag] || CATEGORIES.ops;
// Stage-specific tag label
const stageTag = (() => {
if (stage === 'final') return { label: 'final week — no more moves', cls: 'crit' };
if (stage === 'rolled') return { label: `rolled over · week ${(p.rollovers || 1) + 1}`, cls: 'crit' };
if (stage === 'fresh' && weekend) return { label: 'last chance — closes tomorrow', cls: 'last' };
return null;
})();
return (
{/* ── Top urgency banners ── always visible, can't be missed ── */}
{lastChance && (
⚡
Last chance this week. {' '}
{canTransfer
? 'Close it this weekend or move it to next week — one free move left.'
: 'Transfer already used — you must close this or grant an extension.'}
{canTransfer && (
↪ Move to next week
)}
)}
{stage === 'rolled' && (
🔴
Carried over from last week. {' '}
This must close this week — or use a special one-week extension.
{!p.extended && !extConfirm && (
setExtConfirm(true)}>
+ Grant 1-week extension
)}
{!p.extended && extConfirm && (
This is the final extension.{' '}
{ onExtend(); setExtConfirm(false); }}>
Confirm
{' · '}
setExtConfirm(false)}>
Cancel
)}
)}
{stage === 'final' && (
🔒
{p.extended ? 'Special extension week.' : 'Final week.'} {' '}
No more moves. Close it or cut it — this is the end of the line.
)}
{idx}
{ if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}>
{p.title}
setCatOpen((o) => !o)}
style={{ '--cat-color': cat.color, '--cat-bg': cat.bg, '--cat-fg': cat.fg }}
title="Change category">
{cat.label}
{stageTag && (
• {stageTag.label}
)}
{catOpen && (
setCatOpen(false)}>
Category
{CATEGORY_ORDER.map((k) => {
const c = CATEGORIES[k];
return (
{ onSetCategory(k); setCatOpen(false); }}
className={p.tag === k ? 'sel' : ''}
style={{ '--cat-color': c.color }}>
{c.full}
{c.energyLabel}
);
})}
)}
{p.tasks.map((t) => (
onToggleTask(t)}>
{t.done && }
{t.label}
{ e.stopPropagation(); onBurst(t.id); }}>
⚡ burst
))}
{adding ? (
) : (
setAdding(true)}>
Add next step
)}
{done}/{p.tasks.length}
{stage === 'final' ? '🔒 no more moves' :
stage === 'rolled' ? '⚠ week 2 — extension available' :
lastChance ? '⚡ closes this weekend' :
canTransfer ? 'transfer available' : 'transfer used'}
);
}
function EmptySlot({ onAdd, index }) {
const [adding, setAdding] = React.useState(false);
const [draft, setDraft] = React.useState('');
if (adding) {
return (
);
}
return (
setAdding(true)}>
Add priority #{index}
One outcome you want closed this week
);
}
Object.assign(window, { Priorities });