feat(widgets): add clock, daily summary, and quick actions
This commit is contained in:
1 parent
72c4cfd0c2
commit
47fa2896ba
6 files changed
+115
-4
No files matched your search
@@ -8,7 +8,13 @@
|
||||
const read = () => { try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch { return {}; } };
|
||||
const saveLayout = (id, layout) => localStorage.setItem(KEY, JSON.stringify({ ...read(), [id]: layout }));
|
||||
const positive = value => Number.isInteger(value) && value >= 0;
|
||||
const initial = id => { const { columns } = limits(); return { col: Math.max(0, columns - 3), row: 0, w: 3, h: 3 }; };
|
||||
const initial = id => {
|
||||
const { columns, rows } = limits();
|
||||
if (id === 'clock') return { col: Math.max(0, columns - 5), row: 0, w: 2, h: 2 };
|
||||
if (id === 'today') return { col: Math.max(0, columns - 5), row: Math.min(2, rows - 2), w: 2, h: 3 };
|
||||
if (id === 'actions') return { col: Math.max(0, columns - 3), row: Math.min(3, rows - 2), w: 3, h: 2 };
|
||||
return { col: Math.max(0, columns - 3), row: 0, w: 3, h: 3 };
|
||||
};
|
||||
const normalized = id => {
|
||||
const stored = read()[id], base = initial(id), { columns, rows } = limits();
|
||||
const w = Math.min(columns, Math.max(2, positive(stored?.w) ? stored.w : base.w));
|
||||
@@ -87,6 +93,24 @@
|
||||
}
|
||||
const goal = createWidget('goals', label('goals'));
|
||||
goal.querySelector('.widget-body').append(document.querySelector('#desktop-goal-countdowns'));
|
||||
const clock = createWidget('clock', label('clock'));
|
||||
const today = createWidget('today', label('today'));
|
||||
const actions = createWidget('actions', label('actions'));
|
||||
const widgetIds = ['goals', 'clock', 'today', 'actions'];
|
||||
const defaultVisible = ['goals', 'clock', 'today'];
|
||||
const selected = () => Array.isArray(settings().desktopWidgets) ? widgetIds.filter(id => settings().desktopWidgets.includes(id)) : defaultVisible;
|
||||
function renderWidgets() {
|
||||
const configured = settings().desktopWidgets, visible = selected();
|
||||
const hasGoals = !document.querySelector('#desktop-goal-countdowns').classList.contains('hidden');
|
||||
wrappers.forEach((node, id) => node.classList.toggle('hidden', !visible.includes(id) || (id === 'goals' && !hasGoals && !Array.isArray(configured))));
|
||||
layer.classList.toggle('hidden', [...wrappers.values()].every(node => node.classList.contains('hidden')));
|
||||
const now = new Date();
|
||||
clock.querySelector('.widget-body').innerHTML = `<div class="widget-clock-time">${new Intl.DateTimeFormat(LifeI18n.locale(), { hour: '2-digit', minute: '2-digit' }).format(now)}</div><div class="widget-clock-date">${LifeI18n.date(now, { weekday: 'long', month: 'long', day: 'numeric' })}</div>`;
|
||||
const current = day(), memories = mems().filter(item => item.date === current).length;
|
||||
const tasks = sys().tasksBoard?.tasks || [], due = tasks.filter(item => !item.done && item.dueDate && item.dueDate <= current).length;
|
||||
today.querySelector('.widget-body').innerHTML = `<div class="widget-today-row"><strong>${LifeI18n.number(memories)}</strong><span>${label('memoriesToday')}</span></div><div class="widget-today-row"><strong>${LifeI18n.number(due)}</strong><span>${label('tasksDue')}</span></div><button class="widget-link" onclick="openApp('calendar')">${label('openCalendar')}</button>`;
|
||||
actions.querySelector('.widget-body').innerHTML = `<div class="widget-actions">${[['today','newMemory'],['tasks','openTasks'],['calendar','openCalendar']].map(([id, text]) => `<button onclick="openApp('${id}')">${label(text)}</button>`).join('')}</div>`;
|
||||
}
|
||||
const originalGoals = window.renderDesktopGoalCountdowns;
|
||||
window.renderDesktopGoalCountdowns = () => {
|
||||
originalGoals();
|
||||
@@ -95,10 +119,32 @@
|
||||
const empty = goal.querySelector('.widget-empty-message');
|
||||
if (list.classList.contains('hidden') && !empty) goal.querySelector('.widget-body').insertAdjacentHTML('beforeend', `<button class="widget-empty-message" onclick="openApp('tasks')">${label('noGoals')}</button>`);
|
||||
else if (!list.classList.contains('hidden')) empty?.remove();
|
||||
renderWidgets();
|
||||
};
|
||||
window.renderDesktopGoalCountdowns();
|
||||
const originalRenderAll = window.renderAll;
|
||||
window.renderAll = () => { originalRenderAll(); renderWidgets(); };
|
||||
const originalApplyDesktopPreferences = window.applyDesktopPreferences;
|
||||
window.applyDesktopPreferences = () => { originalApplyDesktopPreferences?.(); renderWidgets(); };
|
||||
window.toggleDesktopWidget = id => {
|
||||
if (!widgetIds.includes(id)) return;
|
||||
const visible = selected(), next = visible.includes(id) ? visible.filter(item => item !== id) : widgetIds.filter(item => visible.includes(item) || item === id);
|
||||
const state = settings(); state.desktopWidgets = next; save('lifeos_settings', state); window.renderAll();
|
||||
};
|
||||
window.resetWidgetLayout = () => { localStorage.removeItem(KEY); wrappers.forEach((_, id) => place(id)); };
|
||||
const originalSystemApp = window.systemApp;
|
||||
window.systemApp = container => {
|
||||
originalSystemApp(container);
|
||||
const panel = container.querySelector('.settings');
|
||||
if (!panel) return;
|
||||
panel.insertAdjacentHTML('beforeend', `<section class="widget-preferences"><div class="eyebrow">${label('heading')}</div><p>${label('hint')}</p><div class="desktop-app-choices">${widgetIds.map(id => `<button type="button" class="chip ${selected().includes(id) ? 'selected' : ''}" aria-pressed="${selected().includes(id)}" onclick="toggleDesktopWidget('${id}')">${label(id)}</button>`).join('')}</div><button class="desktop-text-button" onclick="resetWidgetLayout()">${label('reset')}</button></section>`);
|
||||
};
|
||||
setInterval(renderWidgets, 60000);
|
||||
window.addEventListener('resize', () => wrappers.forEach((_, id) => place(id)));
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `#desktop-widget-layer{position:absolute;inset:0;z-index:1;pointer-events:none}#desktop-snap-grid{position:absolute;z-index:20;left:24px;top:72px;width:calc(100% - 48px);height:calc(100% - 175px);pointer-events:none;opacity:0;transition:opacity .12s;background-image:linear-gradient(to right,color-mix(in srgb,var(--accent) 25%,transparent) 1px,transparent 1px),linear-gradient(to bottom,color-mix(in srgb,var(--accent) 25%,transparent) 1px,transparent 1px);background-size:110px 98px}#desktop-snap-grid.visible{opacity:1}.snap-target{position:absolute;border:2px solid var(--accent);border-radius:13px;background:color-mix(in srgb,var(--accent) 14%,transparent);box-shadow:0 0 0 2px var(--bg)}.desktop-widget{position:absolute;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);backdrop-filter:blur(12px);pointer-events:auto}.desktop-widget.is-adjusting{border-color:var(--accent)}.widget-drag{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none;padding:10px 13px;border-bottom:1px solid var(--line);font:500 11px 'DM Mono';letter-spacing:.1em;color:var(--muted);cursor:grab;touch-action:none}.widget-drag:active{cursor:grabbing}.widget-drag:focus-visible,.widget-resize:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}.widget-body{flex:1;min-height:0;overflow:auto;padding:10px}.widget-resize{position:absolute;right:0;bottom:0;width:22px;height:22px;cursor:nwse-resize;touch-action:none;background:linear-gradient(135deg,transparent 65%,var(--accent) 65%,var(--accent) 72%)}.desktop-widget .desktop-goal-countdowns{position:static!important;width:100%!important;height:auto!important;max-height:none!important;margin:0!important;display:grid;grid-template-columns:1fr;gap:8px;overflow:visible}.desktop-widget .desktop-goal-countdowns.hidden{display:none!important}.desktop-widget .desktop-goal-heading{display:none}.desktop-widget .desktop-goal-card{box-shadow:none;padding:10px 12px}.widget-empty-message{border:0;background:transparent;color:var(--muted);padding:10px;text-align:left;font-size:13px}@media(max-width:650px){#desktop-snap-grid{display:none}#desktop-widget-layer{position:relative;inset:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:10px 14px 90px}.desktop-widget{position:relative;left:auto!important;top:auto!important;width:auto!important;height:auto!important;min-height:140px}.widget-drag{cursor:default}.widget-resize{display:none}.widget-body{max-height:35vh}}`;
|
||||
document.head.append(style);
|
||||
const contentStyle = document.createElement('style');
|
||||
contentStyle.textContent = `.widget-clock-time{font:600 clamp(26px,3vw,42px) Manrope,system-ui;letter-spacing:-.06em;line-height:1.2}.widget-clock-date{font-size:13px;color:var(--muted);line-height:1.5}.widget-today-row{display:flex;align-items:baseline;gap:10px;margin:6px 0}.widget-today-row strong{font-size:25px;color:var(--accent)}.widget-today-row span{font-size:13px;color:var(--muted)}.widget-link{border:0;background:none;color:var(--accent);padding:5px 0;text-decoration:underline;font-size:12px}.widget-actions{display:grid;gap:8px}.widget-actions button{border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:9px;text-align:left;font-size:13px}.widget-actions button:hover,.widget-actions button:focus-visible{border-color:var(--accent)}.widget-preferences .desktop-app-choices{display:flex;gap:8px;flex-wrap:wrap}.widget-preferences .chip{font-size:12px;min-height:36px}`;
|
||||
document.head.append(contentStyle);
|
||||
})();
|
||||
Reference in new issue
Block a user