Files
Life-OS/public/i18n-renderers.js
T

107 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Render-time localisation layer. User-created records are deliberately never translated. */
(() => {
const t = (key, values) => LifeI18n.t(key, values);
const locale = () => lang() === 'zh' ? 'zh-CN' : 'en-US';
const weekday = value => new Intl.DateTimeFormat(locale(), { weekday: 'long' }).format(new Date(value + 'T12:00:00'));
const uiDate = value => new Intl.DateTimeFormat(locale(), { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }).format(new Date(value + 'T12:00:00'));
const short = value => new Intl.DateTimeFormat(locale(), { month: 'short', day: 'numeric' }).format(new Date(value + 'T12:00:00')).toUpperCase();
const weather = mood => t(`weather.${mood}`);
const rootModal = () => document.getElementById('modal');
const linkedTasks = () => sys().tasksBoard?.tasks || [];
const escAttr = value => esc(value).replace(/\"/g, '"');
const titleOf = memory => memory.title?.trim() || memory.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled');
const localizedFilmField = (memory, field) => { const index = LifeI18n.dictionaries.en.prompt.films.findIndex(film => film.title === memory.filmTitle); return index < 0 ? (memory[field] || '') : (LifeI18n.dictionaries[lang() === 'zh' ? 'zh' : 'en'].prompt.films[index]?.[field === 'filmTitle' ? 'title' : 'subtitle'] || memory[field] || ''); };
const tagList = value => [...new Set(String(value || '').split(/[,,]/).map(tag => tag.trim()).filter(Boolean))].slice(0, 12);
const memoryMetaFields = memory => `<div class="memory-meta-form"><label>${t('memory.tags')}<input id="memory-tags" maxlength="180" value="${esc((memory?.tags || []).join(', '))}" placeholder="${t('memory.tagsPlaceholder')}"></label><label>${t('memory.place')}<input id="memory-place" maxlength="100" value="${esc(memory?.location || '')}" placeholder="${t('memory.placePlaceholder')}"></label><label>${t('memory.linkTask')}<select id="memory-task"><option value="">${t('memory.noTask')}</option>${linkedTasks().map(task => `<option value="${esc(task.id)}" ${task.id === memory?.taskId ? 'selected' : ''}>${esc(task.title)}</option>`).join('')}</select></label></div>`;
window.appName = id => t(`app.${id}`);
window.moodName = weather;
window.fmtDate = uiDate;
window.shortDate = short;
window.addMemoryForDate = date => { todayTargetDate = date; todayDraft = null; todayTitleDraft = null; openApp('today'); };
window.closeModal = () => { rootModal().innerHTML = ''; };
window.applyLanguage = () => { document.documentElement.lang = lang() === 'zh' ? 'zh-CN' : 'en'; document.title = t('shell.pageTitle'); const brand = document.querySelector('#desktop-brand'), launcher = document.querySelector('#life-launcher'), boot = document.querySelector('#boot span'); if (brand) brand.textContent = t('shell.brand'); if (launcher) launcher.textContent = t('shell.life'); if (boot) boot.textContent = t('shell.initializing'); if (settings().wallpaper) applyWallpaper(settings().wallpaper); };
window.applyWallpaper = wallpaper => { if (!wallpaper?.url) return; const desktop = document.querySelector('#desktop'); desktop.style.backgroundImage = `linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${wallpaper.url}')`; desktop.style.backgroundSize = 'cover'; desktop.style.backgroundPosition = 'center'; desktop.title = t('shell.wallpaperTitle', { title: wallpaper.title || t('wallpaper.dailyImage') }); };
window.today = container => {
const entryDate = todayTargetDate || day(), memory = mems().find(item => item.date === entryDate);
if (todayDraft === null) { selectedMood = memory?.mood || 'cloudy'; selectedEnergy = memory?.energy || 3; }
const film = filmFor(selectedMood, selectedEnergy), text = todayDraft === null ? (memory?.text || '') : todayDraft;
container.innerHTML = `<div class="eyebrow">${weekday(entryDate).toUpperCase()}</div><h1>${uiDate(entryDate)}</h1><p class="sub">${t('today.prompt')} ${entryDate !== day() ? `<button class="text-link" onclick="todayTargetDate=null;todayDraft=null;todayTitleDraft=null;renderAll()">${t('today.backToToday')}</button>` : ''}</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="${t('today.placeholder')}" oninput="todayDraft=this.value">${esc(text)}</textarea><label class="memory-title-field">${t('memory.title')}<input id="memory-title" maxlength="120" value="${escAttr(todayTitleDraft === null ? (memory?.title || '') : todayTitleDraft)}" oninput="todayTitleDraft=this.value" placeholder="${t('memory.titlePlaceholder')}"></label>${memoryMetaFields(memory)}<div class="actions"><button class="primary" onclick="archiveToday()">${t(entryDate === day() ? (memory ? 'today.update' : 'today.archive') : (memory ? 'today.updateDate' : 'today.archiveDate'))}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">${t('today.weather')}</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(item => `<button class="chip ${item === selectedMood ? 'selected' : ''}" onclick="setMood('${item}')">${weather(item)}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">${t('today.energy')}</div><div class="energy">${[1,2,3,4,5].map(item => `<button class="${item === selectedEnergy ? 'selected' : ''}" onclick="setEnergy(${item})">${item}</button>`).join('')}</div><div class="film"><div class="eyebrow">${t('today.film')}</div><div class="film-title">${film.filmTitle}</div><div class="sub">${film.filmSubtitle}</div><p class="film-copy">“${film.filmLine}”</p></div></aside></div>`;
};
window.archiveToday = () => {
const entryDate = todayTargetDate || day(), text = document.querySelector('#entry')?.value.trim() || '', old = mems().find(item => item.date === entryDate), film = filmFor(selectedMood, selectedEnergy), now = new Date().toISOString();
const memory = { ...old, id: old?.id || `memory_${crypto.randomUUID()}`, date: entryDate, text, title: document.querySelector('#memory-title')?.value.trim() || '', mood: selectedMood, energy: selectedEnergy, tags: tagList(document.querySelector('#memory-tags')?.value), location: document.querySelector('#memory-place')?.value.trim() || '', taskId: document.querySelector('#memory-task')?.value || '', filmTitle: film.filmTitle, filmSubtitle: film.filmSubtitle, filmLine: film.filmLine, createdAt: old?.createdAt || now, updatedAt: now };
const status = document.querySelector('#saveStatus'); status.textContent = t('today.archiving');
setTimeout(() => { save('lifeos_memories', [...mems().filter(item => item.date !== entryDate), memory].sort((a, b) => b.date.localeCompare(a.date))); todayDraft = null; todayTitleDraft = null; status.textContent = t('today.archived'); renderAll(); }, 360);
};
window.timeline = container => {
const all = mems(), tags = [...new Set(all.flatMap(item => item.tags || []))].sort((a,b) => a.localeCompare(b));
const query = window.timelineQuery || '', selectedTag = window.timelineTag || '';
const filtered = all.filter(memory => {
const task = linkedTasks().find(item => item.id === memory.taskId);
const haystack = [memory.title, memory.filmTitle, memory.text, memory.location, ...(memory.tags || []), task?.title].join(' ').toLocaleLowerCase();
return (!query || haystack.includes(query.toLocaleLowerCase())) && (!selectedTag || (memory.tags || []).includes(selectedTag));
});
const isSaved = memory => museum().some(item => item.memoryId === memory.id);
container.innerHTML = `<div class="eyebrow">${t('timeline.eyebrow')}</div><h1>${t('timeline.heading',{count:filtered.length,total:all.length})}</h1><div class="timeline-tools"><label class="timeline-search"><span class="sr-only">${t('timeline.search')}</span><input type="search" value="${esc(query)}" placeholder="${t('timeline.searchPlaceholder')}" oninput="setTimelineQuery(this.value)"></label><label class="timeline-tag-filter"><span class="sr-only">${t('timeline.filterTag')}</span><select onchange="setTimelineTag(this.value)"><option value="">${t('timeline.allTags')}</option>${tags.map(tag => `<option value="${esc(tag)}" ${tag === selectedTag ? 'selected' : ''}>${esc(tag)}</option>`).join('')}</select></label></div>${filtered.length ? `<div class="timeline">${filtered.map(memory => { const task = linkedTasks().find(item => item.id === memory.taskId); return `<article class="memory"><button class="memory-open" onclick="detail('${esc(memory.id)}')"><div class="date">${short(memory.date)} · ${weather(memory.mood).toUpperCase()} · ${t('common.energy')} ${memory.energy}${isSaved(memory) ? ` · ★ ${t('timeline.favorite')}` : ''}</div><div class="mtitle">${esc(titleOf(memory))}</div><div class="snippet">${esc(memory.text || t('timeline.noNote'))}</div><div class="memory-badges">${memory.location ? `<span>⌖ ${esc(memory.location)}</span>` : ''}${task ? `<span>↗ ${esc(task.title)}</span>` : ''}${(memory.tags || []).map(tag => `<span># ${esc(tag)}</span>`).join('')}</div></button></article>`; }).join('')}</div>` : `<div class="empty">${all.length ? t('timeline.noMatches') : `${t('timeline.emptyTitle')}<br>${t('timeline.emptyBody')}`}</div>`}`;
};
window.museumApp = container => {
const saved = museum(), memories = mems();
container.innerHTML = `<div class="eyebrow">${t('museum.eyebrow')}</div><h1>${t('museum.title')}</h1><p class="sub">${t('museum.subtitle')}</p>${saved.length ? `<div class="museum">${saved.map((artifact,index) => { const memory = memories.find(item => item.id === artifact.memoryId); if (!memory) return ''; return `<button class="artifact" onclick="detail('${esc(memory.id)}')"><span>${t('museum.collection')} #${String(index + 1).padStart(2,'0')}</span><b>${esc(titleOf(memory))}</b><small>${LifeI18n.date(memory.date,{dateStyle:'medium'})} · ${t('museum.preserved')}</small></button>`; }).join('')}</div>` : `<div class="empty">${t('museum.empty')}</div>`}`;
};
window.setTimelineQuery = value => { window.timelineQuery = value; const field = document.querySelector('#content-timeline input[type=search]'); const start = field?.selectionStart; timeline(document.querySelector('#content-timeline')); const next = document.querySelector('#content-timeline input[type=search]'); next?.focus(); if (start !== undefined) next?.setSelectionRange(start,start); };
window.setTimelineTag = value => { window.timelineTag = value; timeline(document.querySelector('#content-timeline')); };
window.detail = id => {
const memory = mems().find(item => item.id === id); if (!memory) return;
const task = linkedTasks().find(item => item.id === memory.taskId), favorite = museum().some(item => item.memoryId === memory.id);
rootModal().innerHTML = `<div class="modal" onclick="if(event.target === this) closeModal()"><div class="modalbox" role="dialog" aria-modal="true" aria-labelledby="memory-detail-title"><button class="modal-close" type="button" onclick="closeModal()" aria-label="${t('common.close')}">×</button><div class="eyebrow">${t('timeline.detail')}</div><h1 id="memory-detail-title">${esc(titleOf(memory))}</h1><p class="sub">${uiDate(memory.date)} · ${weather(memory.mood)} · ${t('common.energy')} ${memory.energy}</p><p style="line-height:1.65">${esc(memory.text || t('timeline.noNote')).replace(/\n/g,'<br>')}</p>${memory.location ? `<p class="memory-detail-meta">⌖ ${esc(memory.location)}</p>` : ''}${task ? `<p class="memory-detail-meta">↗ ${esc(t('memory.relatedTask'))}: <button class="text-link" onclick="closeModal();openApp('tasks')">${esc(task.title)}</button></p>` : ''}${(memory.tags || []).length ? `<div class="memory-badges detail-badges">${memory.tags.map(tag => `<span># ${esc(tag)}</span>`).join('')}</div>` : ''}<p class="film-copy"><b>${t('today.film')}: ${esc(localizedFilmField(memory,'filmTitle'))}</b><br>${esc(localizedFilmField(memory,'filmSubtitle'))}</p><div class="actions"><button class="primary" onclick="editMemory('${esc(memory.id)}')">${t('common.edit')}</button><button class="secondary" onclick="preserve('${esc(memory.id)}')">${favorite ? t('timeline.preserved') : t('memory.favorite')}</button><button class="danger" onclick="confirmDelete('${esc(memory.id)}')">${t('common.delete')}</button></div></div></div>`;
};
window.editMemory = id => { const memory = mems().find(item => item.id === id); if (!memory) return; rootModal().innerHTML = `<div class="modal" onclick="if(event.target===this) closeModal()"><form class="modalbox memory-edit-form" onsubmit="saveMemoryEdit(event,'${esc(memory.id)}')" role="dialog" aria-modal="true" aria-labelledby="memory-edit-title"><button type="button" class="modal-close" onclick="closeModal()" aria-label="${t('common.close')}">×</button><div class="eyebrow">${t('timeline.detail')}</div><h2 id="memory-edit-title">${t('memory.edit')}</h2><p class="sub">${uiDate(memory.date)}</p><label>${t('memory.title')}<input id="memory-edit-title" maxlength="120" value="${escAttr(memory.title || '')}" placeholder="${t('memory.titlePlaceholder')}"></label><label>${t('memory.note')}<textarea id="memory-edit-note" rows="5">${esc(memory.text || '')}</textarea></label>${memoryMetaFields(memory)}<div class="actions"><button class="primary" type="submit">${t('common.save')}</button><button class="secondary" type="button" onclick="closeModal()">${t('common.cancel')}</button></div></form></div>`; };
window.saveMemoryEdit = (event,id) => { event.preventDefault(); const all=mems(), memory=all.find(item=>item.id===id); if(!memory)return; memory.title=document.querySelector('#memory-edit-title').value.trim(); memory.text=document.querySelector('#memory-edit-note').value.trim(); memory.tags=tagList(document.querySelector('#memory-tags').value); memory.location=document.querySelector('#memory-place').value.trim(); memory.taskId=document.querySelector('#memory-task').value||''; memory.updatedAt=new Date().toISOString(); save('lifeos_memories',all); closeModal(); renderAll(); detail(id); };
window.preserve = id => { const all = museum(), alreadyPreserved = all.some(item => item.memoryId === id); if (alreadyPreserved) { save('lifeos_museum', all.filter(item => item.memoryId !== id)); toast(t('timeline.unpreserved')); } else { save('lifeos_museum', [...all, { id: `artifact_${crypto.randomUUID()}`, memoryId: id, preservedAt: new Date().toISOString() }]); toast(t('timeline.added')); } closeModal(); renderAll(); };
window.confirmDelete = id => { rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('timeline.confirmDelete')}</div><h2>${t('timeline.remove')}</h2><p class="sub">${t('timeline.irreversible')}</p><div class="actions"><button class="danger" onclick="deleteMemory('${id}')">${t('timeline.deleteMemory')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
window.deleteMemory = id => { save('lifeos_memories', mems().filter(item => item.id !== id)); save('lifeos_museum', museum().filter(item => item.memoryId !== id)); rootModal().innerHTML = ''; renderAll(); toast(t('timeline.removed')); };
window.insights = container => {
const all = mems(), recent = all.filter(item => Date.now() - new Date(item.date + 'T12:00:00') < 2592e6), counts = { clear: 0, light: 0, cloudy: 0, rain: 0, storm: 0 }; recent.forEach(item => counts[item.mood]++);
const average = all.length ? (all.reduce((sum, item) => sum + item.energy, 0) / all.length).toFixed(1) : '—', dayCounts = {}; all.forEach(item => { const name = weekday(item.date); dayCounts[name] = (dayCounts[name] || 0) + 1; }); const active = Object.entries(dayCounts).sort((a,b) => b[1] - a[1])[0]?.[0] || '—';
container.innerHTML = `<div class="eyebrow">${t('insights.eyebrow')}</div><h1>${t('insights.title')}</h1><div class="insights"><section class="metric"><div class="eyebrow">${t('insights.weather')}</div>${Object.entries(counts).map(([key, value]) => `<div class="sub">${weather(key)} <b style="float:right;color:var(--ink)">${value} ${t('common.days')}</b></div>`).join('')}</section><section class="metric"><div class="eyebrow">${t('insights.average')}</div><div class="big">${average}<span style="font-size:16px"> / 5</span></div><div class="bars">${all.slice(0,10).reverse().map(item => `<i class="bar" style="height:${item.energy * 18}%"></i>`).join('') || `<span class="sub">${t('insights.noEntries')}</span>`}</div></section><section class="metric"><div class="eyebrow">${t('insights.active')}</div><div class="big" style="font-size:26px">${active}</div></section><section class="metric"><div class="eyebrow">${t('insights.current')}</div><div class="big">${streak(all)}<span style="font-size:16px"> ${t('common.days')}</span></div><div class="sub">${t('insights.longest',{count:longest(all)})}</div></section></div>`;
};
window.profileApp = container => {
const profileData = profile(), all = mems(), common = Object.entries(all.reduce((out, item) => (out[item.mood] = (out[item.mood] || 0) + 1, out), {})).sort((a,b) => b[1] - a[1])[0]?.[0] || '—', average = all.length ? (all.reduce((sum,item) => sum + item.energy,0) / all.length).toFixed(1) : '—', started = Math.max(1, Math.floor((Date.now() - new Date(profileData.startDate)) / 864e5));
container.innerHTML = `<div class="eyebrow">${t('profile.eyebrow')}</div><h1>${profileData.name ? esc(profileData.name) + ' · ' : ''}${t('profile.online')}</h1><div class="profile-grid"><div class="stat"><b>${started}</b><span>${t('profile.daysOnline')}</span></div><div class="stat"><b>${all.length}</b><span>${t('profile.memories')}</span></div><div class="stat"><b>${museum().length}</b><span>${t('profile.museumItems')}</span></div><div class="stat"><b>${streak(all)}</b><span>${t('profile.streak')}</span></div><div class="stat"><b>${longest(all)}</b><span>${t('profile.longest')}</span></div><div class="stat"><b>${common === '—' ? common : weather(common)}</b><span>${t('profile.commonWeather')}</span></div></div><div class="eyebrow">${t('profile.personal')}</div><div class="formrow"><label>${t('profile.name')}</label><input id="pname" value="${esc(profileData.name)}"></div><div class="formrow"><label>${t('profile.birthday')}</label><input id="pbday" type="date" value="${esc(profileData.birthday)}"></div><div class="formrow"><label>${t('profile.start')}</label><input id="pstart" type="date" value="${esc(profileData.startDate)}"></div><div class="formrow"><label>${t('profile.motto')}</label><input id="pmotto" value="${esc(profileData.motto)}"></div><div class="actions"><button class="primary" onclick="saveProfile()">${t('profile.save')}</button><span class="status">${t('profile.average',{value:average})}</span></div>`;
};
window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); };
window.systemApp = container => {
const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn');
container.innerHTML = `<div class="eyebrow">${t('system.eyebrow')}</div><h1>${t('system.title')}</h1><div class="settings"><section><div class="eyebrow">${t('system.appearance')}</div><div class="choices">${['dark','light','auto'].map(item => `<button class="chip ${state.theme === item ? 'selected' : ''}" onclick="theme('${item}')">${t(`system.theme.${item}`)}</button>`).join('')}</div></section><section><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices"><button class="chip ${preference === 'auto' ? 'selected' : ''}" onclick="window.setLanguage('auto')">${t('system.auto')}</button><button class="chip ${preference === 'en' ? 'selected' : ''}" onclick="window.setLanguage('en')">${t('system.languageEnglish')}</button><button class="chip ${preference === 'zh' ? 'selected' : ''}" onclick="window.setLanguage('zh')">${t('system.languageChinese')}</button></div></section><section><div class="eyebrow">${t('system.data')}</div><div class="actions"><button class="primary" onclick="exportData()">${t('system.export')}</button><button class="secondary" onclick="document.querySelector('#importer').click()">${t('system.import')}</button><button class="danger" onclick="resetConfirm()">${t('system.reset')}</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">${t('system.storage')}</div><p>${t('system.local')}<br>${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}</p></section><section><div class="eyebrow">${t('system.dataStorage')}</div><p>${t('system.stored')}<br>${t('system.backups')}</p><p>${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `<br><b style="color:var(--accent)">${t('system.recommended')}</b>` : ''}</p></section><section><div class="eyebrow">${t('system.version')}</div><p>${t('system.release',{version:window.LifeOSVersion||'1.5.7'})}</p></section></div>`;
};
window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('dialog.backup')}</div><h2>${t('dialog.ready')}</h2><p class="sub">${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})}<br>${t('dialog.memories',{count:data.memories.length})}<br>${t('dialog.museumItems',{count:data.museum.length})}<br>${t('dialog.goals',{count:board.goals?.length || 0})}<br>${t('dialog.tasks',{count:board.tasks?.length || 0})}</p><div class="actions"><button class="primary" onclick="applyImport('merge')">${t('dialog.merge')}</button><button class="danger" onclick="replaceConfirm()">${t('dialog.replaceAll')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
window.replaceConfirm = () => { rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('dialog.final')}</div><h2>${t('dialog.replaceQuestion')}</h2><p class="sub">${t('dialog.replaceWarning')}</p><div class="actions"><button class="danger" onclick="applyImport('replace')">${t('dialog.replace')}</button><button class="secondary" onclick="importPreview()">${t('common.cancel')}</button></div></div></div>`; };
window.applyImport = mode => {
const data = importData;
if (mode === 'replace') {
save('lifeos_memories', data.memories); save('lifeos_museum', data.museum); save('lifeos_profile', data.profile || {}); save('lifeos_settings', data.settings || {}); save('lifeos_system', data.system || { schemaVersion: 2 });
} else {
const memories = mems(), ids = new Set(memories.map(item => item.id)), dates = new Set(memories.map(item => item.date));
save('lifeos_memories', [...memories, ...data.memories.filter(item => !ids.has(item.id) && !dates.has(item.date))]);
const artifacts = museum(), artifactIds = new Set(artifacts.map(item => item.id));
save('lifeos_museum', [...artifacts, ...data.museum.filter(item => !artifactIds.has(item.id))]);
const current = sys(), importedBoard = data.system?.tasksBoard;
if (importedBoard && Array.isArray(importedBoard.goals) && Array.isArray(importedBoard.tasks)) {
current.tasksBoard ||= { goals: [], tasks: [] };
for (const kind of ['goals', 'tasks']) { const known = new Set((current.tasksBoard[kind] || []).map(item => item.id)); current.tasksBoard[kind] = [...(current.tasksBoard[kind] || []), ...importedBoard[kind].filter(item => !known.has(item.id))]; }
save('lifeos_system', current);
}
}
rootModal().innerHTML = ''; toast('feedback.restoring'); setTimeout(() => { renderAll(); toast('feedback.restored'); }, 500);
};
window.resetConfirm = () => { rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('system.reset')}</div><h2>${t('dialog.resetQuestion')}</h2><p class="sub">${t('dialog.resetWarning')}</p><div class="actions"><button class="danger" onclick="['lifeos_memories','lifeos_museum','lifeos_profile','lifeos_settings','lifeos_system'].forEach(k=>localStorage.removeItem(k));location.reload()">${t('dialog.resetData')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
window.toast = key => { const message = key.includes('.') ? t(key) : key; const node = document.createElement('div'); node.className = 'toast'; node.textContent = message; document.body.append(node); setTimeout(() => node.remove(), 2000); };
window.minWin = id => { const windowNode = windows[id]; if (!windowNode) return; windowNode.classList.add('min'); if (activeWindow === id) { activeWindow = Object.keys(windows).find(key => key !== id && !windows[key].classList.contains('min')) || null; if (activeWindow) focus(windows[activeWindow]); } renderTaskbar(); toast('feedback.minimized'); };
const memoryStyle = document.createElement('style');
memoryStyle.textContent = `.memory-meta-form{display:grid;gap:9px;margin-top:12px}.memory-title-field{display:grid;gap:6px;margin-top:10px;color:var(--muted);font-size:12px}.memory-title-field input,.memory-edit-form input{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-title-field input:focus,.memory-edit-form input:focus{border-color:var(--accent)}.memory-meta-form label,.memory-edit-form>label{display:grid;gap:6px;color:var(--muted);font-size:12px}.memory-meta-form input,.memory-meta-form select,.memory-edit-form input,.memory-edit-form textarea{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-meta-form input:focus,.memory-meta-form select:focus,.memory-edit-form input:focus,.memory-edit-form textarea:focus,.timeline-tools input:focus,.timeline-tools select:focus{border-color:var(--accent)}.memory-edit-form{display:grid;gap:12px;max-height:min(80vh,720px);overflow:auto}.memory-edit-form h2{font-size:21px;margin:0}.memory-edit-form textarea{resize:vertical;line-height:1.55}.memory-edit-form .memory-meta-form{margin:0}.memory-edit-form .actions{margin:0}.timeline-tools{display:grid;grid-template-columns:minmax(0,1fr) 190px;gap:8px;margin:17px 0 11px}.timeline-tools input,.timeline-tools select{width:100%;min-width:0;height:40px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:0 11px;outline:none;font:13px Manrope,system-ui}.timeline-tools input::placeholder{color:var(--muted)}.memory-open{display:block!important;text-align:left}.memory-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.memory-badges span,.memory-detail-meta{color:var(--muted);font-size:12px}.memory-badges span{border:1px solid var(--line);background:var(--panel2);border-radius:99px;padding:4px 8px}.detail-badges{margin:0 0 14px}.text-link{border:0;background:none;padding:0;color:var(--accent);text-decoration:underline}.memory-detail-meta{margin:8px 0}.memory-edit-form .sub{margin:0}@media(max-width:600px){.timeline-tools{grid-template-columns:1fr 130px}.memory-badges span{font-size:11px}.memory-meta-form label,.memory-edit-form>label,.memory-title-field{font-size:13px}}`;
document.head.append(memoryStyle);
// Render every surface again after the overrides are installed; no DOM walking is involved.
window.renderIcons(); window.renderAll(); window.applyLanguage();
})();