// ===== CONFIG =====
const DOT_SIZE = 15
const DOT_SPACING_X = 6
const DOT_SPACING_Y = 6
const COLOR_DONE = "#ff7a00"
const COLOR_LEFT = "#ffffff"
const COLOR_REMINDER = "#ff0000"
const BACKGROUND = "#000000"
// ==================
let today = new Date()
let day = today.getDate()
let year = today.getFullYear()
let month = today.getMonth()
let daysInMonth = new Date(year, month + 1, 0).getDate()
// 🔥 FETCH REMINDERS
let calendars = await Calendar.forReminders()
let reminders = await Reminder.allIncomplete(calendars)
let reminderDays = new Set()
for (let r of reminders) {
if (!r.dueDate) continue
let d = new Date(r.dueDate)
if (d.getMonth() !== month || d.getFullYear() !== year) continue
if (r.recurrenceRules) {
let hasDaily = r.recurrenceRules.some(rule => rule.frequency === "daily")
if (hasDaily) continue
}
reminderDays.add(d.getDate())
}
// ==================
function makeSplitDot(size, colorL, colorR) {
let scale = 4
let s = size * scale
let ctx = new DrawContext()
ctx.size = new Size(s, s)
ctx.opaque = false
ctx.respectScreenScale = false
let cx = s / 2
let cy = s / 2
let r = s / 2
for (let x = 0; x < s; x++) {
let dx = x - cx + 0.5
if (Math.abs(dx) > r) continue
let halfH = Math.sqrt(r * r - dx * dx)
let top = cy - halfH
let height = halfH * 2
let col = (x < s / 2) ? new Color(colorL) : new Color(colorR)
ctx.setFillColor(col)
ctx.fillRect(new Rect(x, top, 1, height))
}
return ctx.getImage()
}
// ==================
let widget = new ListWidget()
widget.backgroundColor = new Color(BACKGROUND)
widget.addSpacer()
let container = widget.addStack()
container.layoutVertically()
container.centerAlignContent()
let rows = Math.ceil(daysInMonth / 7)
for (let r = 0; r < rows; r++) {
let row = container.addStack()
row.layoutHorizontally()
row.centerAlignContent()
for (let c = 0; c < 7; c++) {
let index = r * 7 + c + 1
if (index > daysInMonth) break
if (reminderDays.has(index) && index === day) {
let img = makeSplitDot(DOT_SIZE, COLOR_DONE, COLOR_REMINDER)
let dotImg = row.addImage(img)
dotImg.imageSize = new Size(DOT_SIZE * 0.80, DOT_SIZE * 0.80)
} else if (reminderDays.has(index) && index > day) {
let dot = row.addText("●")
dot.textColor = new Color(COLOR_REMINDER)
dot.font = Font.systemFont(DOT_SIZE)
} else if (index <= day) {
let dot = row.addText("●")
dot.textColor = new Color(COLOR_DONE)
dot.font = Font.systemFont(DOT_SIZE)
} else {
let dot = row.addText("●")
dot.textColor = new Color(COLOR_LEFT)
dot.font = Font.systemFont(DOT_SIZE)
}
row.addSpacer(DOT_SPACING_X)
}
container.addSpacer(DOT_SPACING_Y)
}
widget.addSpacer()
Script.setWidget(widget)
Script.complete()''',
},
{
'id': 'days-left-year',
'name': 'Days Left in Year',
'tagline': 'Year progress at a glance',
'description': 'Shows exactly how many days remain in the year, alongside your current day-of-year, percentage done, and a full-width progress bar. Styled in cream and burnt orange.',
'size': 'Medium',
'accent': '#C8621A',
'preview': 'https://res.cloudinary.com/dpt9t6vvg/image/upload/widgets/days-left-year.png',
'code': r'''// Days Left in Year Widget — Medium Size
const now = new Date();
const year = now.getFullYear();
const endOfYear = new Date(year, 11, 31); // Dec 31
// Calculate days
const msPerDay = 1000 * 60 * 60 * 24;
const daysLeft = Math.ceil((endOfYear - now) / msPerDay);
const dayOfYear = Math.floor((now - new Date(year, 0, 1)) / msPerDay) + 1;
const totalDays = ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 366 : 365;
const progress = dayOfYear / totalDays;
const percentDone = Math.round(progress * 100);
// Month name
const monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const monthName = monthNames[now.getMonth()];
const dateStr = `${monthName} ${now.getDate()}, ${year}`;
// Colors
const bg = new Color("#F5F0E8"); // Cream
const accentColor = new Color("#C8621A"); // Brownish orange
const textColor = new Color("#1C1C1C"); // Matte black
const subColor = new Color("#4A4A4A"); // Soft matte black
const trackColor = new Color("#DDD8CE"); // Cream track
const widget = new ListWidget();
widget.backgroundColor = bg;
widget.setPadding(16, 18, 16, 18);
// Top row: header label + date
const topRow = widget.addStack();
topRow.layoutHorizontally();
topRow.centerAlignContent();
const headerLabel = topRow.addText("📅 DAYS LEFT IN YEAR");
headerLabel.font = Font.boldMonospacedSystemFont(10);
headerLabel.textColor = subColor;
topRow.addSpacer();
const dateLabel = topRow.addText(dateStr);
dateLabel.font = Font.systemFont(10);
dateLabel.textColor = subColor;
widget.addSpacer(8);
// Main row: big number on left, stats on right
const mainRow = widget.addStack();
mainRow.layoutHorizontally();
mainRow.centerAlignContent();
// Left: big number
const leftStack = mainRow.addStack();
leftStack.layoutVertically();
const numText = leftStack.addText(String(daysLeft));
numText.font = Font.boldSystemFont(64);
numText.textColor = accentColor;
numText.minimumScaleFactor = 0.5;
const daysLabel = leftStack.addText("days remaining");
daysLabel.font = Font.systemFont(11);
daysLabel.textColor = subColor;
mainRow.addSpacer();
// Right: stats column
const rightStack = mainRow.addStack();
rightStack.layoutVertically();
// Day of year
const stat1 = rightStack.addText(`Day ${dayOfYear}`);
stat1.font = Font.boldSystemFont(20);
stat1.textColor = textColor;
stat1.rightAlignText();
const stat1sub = rightStack.addText(`of ${totalDays} days`);
stat1sub.font = Font.systemFont(10);
stat1sub.textColor = subColor;
stat1sub.rightAlignText();
rightStack.addSpacer(10);
// Percent done
const stat2 = rightStack.addText(`${percentDone}% done`);
stat2.font = Font.boldSystemFont(16);
stat2.textColor = accentColor;
stat2.rightAlignText();
const stat2sub = rightStack.addText(`${100 - percentDone}% to go`);
stat2sub.font = Font.systemFont(10);
stat2sub.textColor = subColor;
stat2sub.rightAlignText();
widget.addSpacer(10);
// Progress bar (full width)
const barBg = widget.addStack();
barBg.layoutHorizontally();
barBg.backgroundColor = trackColor;
barBg.cornerRadius = 5;
barBg.size = new Size(0, 8);
const filled = barBg.addStack();
filled.backgroundColor = accentColor;
filled.cornerRadius = 5;
const filledWidth = Math.max(6, Math.round(progress * 300));
filled.size = new Size(filledWidth, 8);
// Refresh every hour
widget.refreshAfterDate = new Date(Date.now() + 60 * 60 * 1000);
if (config.runsInWidget) {
Script.setWidget(widget);
} else {
widget.presentMedium();
}
Script.complete();