feat: категории задач, длительность, личные события в расписании
This commit is contained in:
parent
1d2281a0ec
commit
c9ef8a852b
192
app.js
192
app.js
@ -50,11 +50,38 @@ const PRIORITY = {
|
||||
high: {label:'🔴 Высокая', cls:'bg-red-50 text-red-600'},
|
||||
};
|
||||
|
||||
const CATEGORIES = [
|
||||
{key:'home', label:'🏠 Дом', color:'#10b981'},
|
||||
{key:'work', label:'💼 Работа', color:'#3b82f6'},
|
||||
{key:'study', label:'📚 Учёба', color:'#8b5cf6'},
|
||||
{key:'shop', label:'🛒 Покупки', color:'#f59e0b'},
|
||||
{key:'kids', label:'👶 Дети', color:'#ec4899'},
|
||||
{key:'money', label:'💰 Финансы', color:'#06b6d4'},
|
||||
{key:'health', label:'❤️ Здоровье', color:'#ef4444'},
|
||||
{key:'other', label:'🎉 Другое', color:'#6366f1'},
|
||||
];
|
||||
function catByKey(k){ return CATEGORIES.find(c=>c.key===k) || CATEGORIES[CATEGORIES.length-1]; }
|
||||
function catOptions(sel){ return CATEGORIES.map(c=>`<option value="${c.key}">${c.label}</option>`).join(''); }
|
||||
function formatDuration(min){
|
||||
min = Number(min); if(!min) return '';
|
||||
if(min<60) return min+' мин';
|
||||
const h=Math.floor(min/60), m=min%60;
|
||||
return m? `${h} ч ${m} мин` : `${h} ч`;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
ИНИЦИАЛИЗАЦИЯ
|
||||
============================================================ */
|
||||
function init(){
|
||||
data = load();
|
||||
if(data && data.members && data.members.length){
|
||||
// миграции
|
||||
data.events = data.events || [];
|
||||
(data.tasks||[]).forEach(t=>{
|
||||
if(!t.category) t.category = 'other';
|
||||
if(t.duration===undefined) t.duration = null;
|
||||
});
|
||||
}
|
||||
if(!data || !data.members || !data.members.length){ showOnboard(); return; }
|
||||
me = localStorage.getItem(SESSION);
|
||||
if(!me || !member(me)) me = data.members[0].id;
|
||||
@ -89,7 +116,7 @@ $('#ob-create').onclick = ()=>{
|
||||
const names = $$('#ob-members .ob-name').map(i=>i.value.trim()).filter(Boolean);
|
||||
if(names.length<1){ toast('Добавьте хотя бы одного члена семьи'); return; }
|
||||
const members = names.map((n,i)=>({ id:uid(), name:n, color:AVATAR_COLORS[i%AVATAR_COLORS.length], emoji:AVATAR_EMOJIS[i%AVATAR_EMOJIS.length] }));
|
||||
data = { familyName, members, tasks:[] };
|
||||
data = { familyName, members, tasks:[], events:[] };
|
||||
me = members[0].id;
|
||||
localStorage.setItem(SESSION, me);
|
||||
save();
|
||||
@ -168,12 +195,15 @@ function taskRow(t){
|
||||
const from = member(t.assignerId);
|
||||
const overdue = t.status==='todo' && t.dueDate && parseDate(t.dueDate) < new Date(todayStr());
|
||||
const st = STATUS[t.status] || STATUS.todo;
|
||||
const cat = catByKey(t.category);
|
||||
const dur = formatDuration(t.duration);
|
||||
return `
|
||||
<div class="task ${t.status==='done'?'done':''} ${t.status==='cancelled'?'cancelled':''} ${overdue?'overdue':''}" data-id="${t.id}">
|
||||
<div class="flex items-start gap-3">
|
||||
<button class="iconbtn primary t-done" title="Отметить выполненной" ${t.status==='done'?'disabled':''}>✓</button>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start gap-2 flex-wrap">
|
||||
<span class="chip" style="background:${cat.color}22;color:${cat.color}">${cat.label}</span>
|
||||
<div class="t-title font-semibold flex-1 min-w-0">${escapeHtml(t.title)}</div>
|
||||
<span class="badge ${st.cls}">${st.label}</span>
|
||||
</div>
|
||||
@ -182,6 +212,7 @@ function taskRow(t){
|
||||
<span class="chip" style="background:${(a&&a.color)||'#94a3b8'}22;color:${(a&&a.color)||'#94a3b8'}">${avatarHtml(a,16)} ${escapeHtml(a?a.name:'—')}</span>
|
||||
${from?`<span class="text-muted">от ${escapeHtml(from.name)}</span>`:''}
|
||||
${t.dueDate?`<span class="badge ${overdue?'bg-red-100 text-red-700':'bg-line text-muted'}">⏰ ${prettyDateTime(t.dueDate,t.time)} · ${relativeDue(t.dueDate)}</span>`:''}
|
||||
${dur?`<span class="badge bg-gray-100 text-gray-600">⏱ ${dur}</span>`:''}
|
||||
<span class="badge ${PRIORITY[t.priority].cls}">${PRIORITY[t.priority].label}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -251,34 +282,49 @@ function renderAll(){
|
||||
<div class="flex flex-wrap gap-2 mb-4" id="filters">
|
||||
<select id="flt-member" class="field !w-auto !py-2"><option value="">Все члены</option>${data.members.map(m=>`<option value="${m.id}">${escapeHtml(m.name)}</option>`).join('')}</select>
|
||||
<select id="flt-status" class="field !w-auto !py-2"><option value="">Любой статус</option>${statuses.map(s=>`<option value="${s}">${STATUS[s].label}</option>`).join('')}</select>
|
||||
<select id="flt-cat" class="field !w-auto !py-2"><option value="">Все виды</option>${CATEGORIES.map(c=>`<option value="${c.key}">${c.label}</option>`).join('')}</select>
|
||||
</div>
|
||||
<div id="list" class="space-y-3"></div>`;
|
||||
const apply = ()=>{
|
||||
const fm = $('#flt-member').value, fs = $('#flt-status').value;
|
||||
const fm = $('#flt-member').value, fs = $('#flt-status').value, fc = $('#flt-cat').value;
|
||||
let tasks = data.tasks.slice().sort((a,b)=>(b.createdAt||0)-(a.createdAt||0));
|
||||
if(fm) tasks = tasks.filter(t=>t.assigneeId===fm||t.assignerId===fm);
|
||||
if(fs) tasks = tasks.filter(t=>t.status===fs);
|
||||
if(fc) tasks = tasks.filter(t=>t.category===fc);
|
||||
const list = $('#list');
|
||||
if(!tasks.length){ list.innerHTML = emptyHtml('🗂','Задач не найдено по фильтру.'); return; }
|
||||
list.innerHTML = tasks.map(taskRow).join('');
|
||||
list.querySelectorAll('.task').forEach(bindTaskRow);
|
||||
};
|
||||
$('#flt-member').onchange = apply; $('#flt-status').onchange = apply;
|
||||
$('#flt-member').onchange = apply; $('#flt-status').onchange = apply; $('#flt-cat').onchange = apply;
|
||||
apply();
|
||||
}
|
||||
|
||||
/* ---------- Расписание (календарь) ---------- */
|
||||
/* ---------- Расписание (календарь + личные события) ---------- */
|
||||
let calRef = new Date();
|
||||
let selDay = todayStr();
|
||||
let schedScope = 'mine'; // 'mine' | 'all'
|
||||
|
||||
function dayItems(ds, scope){
|
||||
let evs = (data.events||[]).filter(e=>e.date===ds);
|
||||
let tks = data.tasks.filter(t=>t.dueDate===ds && t.status!=='cancelled');
|
||||
if(scope==='mine'){
|
||||
evs = evs.filter(e=>e.ownerId===me);
|
||||
tks = tks.filter(t=>t.assigneeId===me);
|
||||
}
|
||||
return [
|
||||
...evs.map(e=>({type:'event', sort:e.start||'00:00', obj:e})),
|
||||
...tks.map(t=>({type:'task', sort:t.time||'zz', obj:t})),
|
||||
].sort((a,b)=>a.sort.localeCompare(b.sort));
|
||||
}
|
||||
|
||||
function renderSchedule(){
|
||||
if(!selDay) selDay = todayStr();
|
||||
const y = calRef.getFullYear(), m = calRef.getMonth();
|
||||
const first = new Date(y,m,1);
|
||||
const startDow = (first.getDay()+6)%7; // понедельник = 0
|
||||
const startDow = (first.getDay()+6)%7;
|
||||
const daysInMonth = new Date(y,m+1,0).getDate();
|
||||
const monthName = first.toLocaleDateString('ru-RU',{month:'long', year:'numeric'});
|
||||
const tasksByDate = {};
|
||||
data.tasks.filter(t=>t.dueDate && t.status!=='cancelled').forEach(t=>{
|
||||
(tasksByDate[t.dueDate] = tasksByDate[t.dueDate]||[]).push(t);
|
||||
});
|
||||
let cells = '';
|
||||
const dows = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
|
||||
cells += dows.map(d=>`<div class="cal-dow">${d}</div>`).join('');
|
||||
@ -286,32 +332,137 @@ function renderSchedule(){
|
||||
const today = todayStr();
|
||||
for(let d=1; d<=daysInMonth; d++){
|
||||
const ds = fmt(new Date(y,m,d));
|
||||
const tlist = tasksByDate[ds]||[];
|
||||
cells += `<div class="cal-day ${ds===today?'today':''}">
|
||||
const items = dayItems(ds, schedScope);
|
||||
cells += `<div class="cal-day ${ds===today?'today':''} ${ds===selDay?'selected':''}" data-day="${ds}">
|
||||
<div class="cal-dnum">${d}</div>
|
||||
${tlist.slice(0,3).map(t=>`<div class="cal-task" style="background:${colorFor(t.assigneeId)}22;color:${colorFor(t.assigneeId)};${t.status==='done'?'text-decoration:line-through;opacity:.6':''}" data-id="${t.id}">${escapeHtml(t.title)}</div>`).join('')}
|
||||
${tlist.length>3?`<div class="text-xs text-muted">+${tlist.length-3} ещё</div>`:''}
|
||||
${items.slice(0,3).map(it=>chipHtml(it)).join('')}
|
||||
${items.length>3?`<div class="text-xs text-muted">+${items.length-3}</div>`:''}
|
||||
</div>`;
|
||||
}
|
||||
$('#main').innerHTML = `
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center justify-between mb-4 gap-2 flex-wrap">
|
||||
<h2 class="text-2xl font-extrabold capitalize">${monthName}</h2>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex bg-line rounded-lg p-1">
|
||||
<button id="sc-mine" class="seg ${schedScope==='mine'?'active':''}">Мои</button>
|
||||
<button id="sc-all" class="seg ${schedScope==='all'?'active':''}">Вся семья</button>
|
||||
</div>
|
||||
<button id="cal-prev" class="iconbtn">‹</button>
|
||||
<button id="cal-today" class="btn-ghost text-sm">Сегодня</button>
|
||||
<button id="cal-next" class="iconbtn">›</button>
|
||||
<button id="cal-addev" class="btn-primary text-sm">+ Событие</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cal-grid">${cells}</div>
|
||||
<div id="day-panel" class="mt-5"></div>
|
||||
<div class="flex flex-wrap gap-3 mt-4 text-xs text-muted">
|
||||
${data.members.map(m=>`<span class="flex items-center gap-1">${avatarHtml(m,14)} ${escapeHtml(m.name)}</span>`).join('')}
|
||||
${CATEGORIES.map(c=>`<span class="flex items-center gap-1"><span class="dot" style="background:${c.color}"></span>${c.label}</span>`).join('')}
|
||||
</div>`;
|
||||
$('#cal-prev').onclick=()=>{ calRef=new Date(y,m-1,1); renderSchedule(); };
|
||||
$('#cal-next').onclick=()=>{ calRef=new Date(y,m+1,1); renderSchedule(); };
|
||||
$('#cal-today').onclick=()=>{ calRef=new Date(); renderSchedule(); };
|
||||
$$('#main .cal-task').forEach(el=>el.onclick=()=>openTaskModal(el.dataset.id));
|
||||
$('#cal-today').onclick=()=>{ calRef=new Date(); selDay=todayStr(); renderSchedule(); };
|
||||
$('#cal-addev').onclick=()=>openEventModal(null, selDay);
|
||||
$('#sc-mine').onclick=()=>{ schedScope='mine'; renderSchedule(); };
|
||||
$('#sc-all').onclick=()=>{ schedScope='all'; renderSchedule(); };
|
||||
$$('#main .cal-day[data-day]').forEach(el=>el.onclick=e=>{ if(e.target.closest('.cal-task')) return; selDay=el.dataset.day; renderSchedule(); });
|
||||
$$('#main .cal-task').forEach(el=>el.onclick=e=>{ e.stopPropagation(); el.dataset.type==='event'?openEventModal(el.dataset.id):openTaskModal(el.dataset.id); });
|
||||
renderDayPanel();
|
||||
}
|
||||
|
||||
function chipHtml(it){
|
||||
if(it.type==='event'){
|
||||
const c=catByKey(it.obj.category);
|
||||
return `<div class="cal-task" style="background:${c.color}22;color:${c.color}" data-type="event" data-id="${it.obj.id}">${escapeHtml(it.obj.start)} ${escapeHtml(it.obj.title)}</div>`;
|
||||
}
|
||||
const t=it.obj;
|
||||
return `<div class="cal-task" style="background:${colorFor(t.assigneeId)}22;color:${colorFor(t.assigneeId)};${t.status==='done'?'text-decoration:line-through;opacity:.6':''}" data-type="task" data-id="${t.id}">✓ ${escapeHtml(t.title)}</div>`;
|
||||
}
|
||||
|
||||
function renderDayPanel(){
|
||||
const panel = $('#day-panel'); if(!panel) return;
|
||||
const items = dayItems(selDay, schedScope);
|
||||
const d = parseDate(selDay);
|
||||
const dname = d ? d.toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long'}) : selDay;
|
||||
const isToday = selDay===todayStr();
|
||||
panel.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||
<h3 class="font-bold text-lg capitalize">${dname} ${isToday?'<span class="badge bg-accent-50 text-ink">сегодня</span>':''}</h3>
|
||||
<button id="dp-addev" class="btn-ghost text-sm">+ событие в этот день</button>
|
||||
</div>
|
||||
${items.length? `<div class="space-y-2">${items.map(it=>dayRowHtml(it)).join('')}</div>` : emptyHtml('📅','На этот день ничего не запланировано.')}`;
|
||||
$('#dp-addev')?.addEventListener('click', ()=>openEventModal(null, selDay));
|
||||
panel.querySelectorAll('.day-row').forEach(r=>{
|
||||
r.onclick=()=>{ r.dataset.type==='event'?openEventModal(r.dataset.id):openTaskModal(r.dataset.id); };
|
||||
});
|
||||
}
|
||||
|
||||
function dayRowHtml(it){
|
||||
if(it.type==='event'){
|
||||
const e=it.obj, c=catByKey(e.category), owner=member(e.ownerId);
|
||||
return `<div class="day-row stat-card flex items-center gap-3" data-type="event" data-id="${e.id}" style="cursor:pointer">
|
||||
<div class="time-badge" style="background:${c.color}1a;color:${c.color}">${escapeHtml(e.start)}<br><span class="opacity-60">${escapeHtml(e.end)}</span></div>
|
||||
<div class="dot" style="background:${c.color}"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-semibold">${escapeHtml(e.title)}</div>
|
||||
<div class="text-xs text-muted">${avatarHtml(owner,14)} ${escapeHtml(owner?owner.name:'—')} · ${c.label}</div>
|
||||
</div>
|
||||
<span class="text-muted">✎</span>
|
||||
</div>`;
|
||||
}
|
||||
const t=it.obj, cat=catByKey(t.category), a=member(t.assigneeId);
|
||||
return `<div class="day-row stat-card flex items-center gap-3" data-type="task" data-id="${t.id}" style="cursor:pointer">
|
||||
<div class="time-badge" style="background:${colorFor(t.assigneeId)}1a;color:${colorFor(t.assigneeId)}">${escapeHtml(t.time||'—')}</div>
|
||||
<div class="dot" style="background:${colorFor(t.assigneeId)}"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-semibold ${t.status==='done'?'done':''}">${escapeHtml(t.title)}</div>
|
||||
<div class="text-xs text-muted">${avatarHtml(a,14)} ${escapeHtml(a?a.name:'—')} · ${cat.label}${t.duration?' · ⏱ '+formatDuration(t.duration):''}</div>
|
||||
</div>
|
||||
<span class="badge ${STATUS[t.status].cls}">${STATUS[t.status].label}</span>
|
||||
<span class="text-muted">✎</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ---------- модалка события ---------- */
|
||||
let editingEventId = null;
|
||||
function openEventModal(id, dateStr){
|
||||
editingEventId = id;
|
||||
const e = id ? data.events.find(x=>x.id===id) : null;
|
||||
$('#ev-title').textContent = e ? 'Событие' : 'Новое событие';
|
||||
$('#ev-name').value = e?.title||'';
|
||||
$('#ev-date').value = e?.date || dateStr || selDay || todayStr();
|
||||
$('#ev-start').value = e?.start||'';
|
||||
$('#ev-end').value = e?.end||'';
|
||||
$('#ev-owner').innerHTML = data.members.map(m=>`<option value="${m.id}" ${(e?e.ownerId===m.id:m.id===me)?'selected':''}>${escapeHtml(m.name)}</option>`).join('');
|
||||
$('#ev-cat').innerHTML = catOptions();
|
||||
$('#ev-cat').value = e?.category||'other';
|
||||
$('#ev-delete').classList.toggle('hidden', !e);
|
||||
$('#event-modal').classList.remove('hidden');
|
||||
setTimeout(()=>$('#ev-name').focus(),50);
|
||||
}
|
||||
$('#ev-close').onclick = ()=>$('#event-modal').classList.add('hidden');
|
||||
$('#event-modal').addEventListener('click', e=>{ if(e.target.id==='event-modal') $('#event-modal').classList.add('hidden'); });
|
||||
$('#ev-save').onclick = ()=>{
|
||||
const title=$('#ev-name').value.trim();
|
||||
const date=$('#ev-date').value;
|
||||
const start=$('#ev-start').value, end=$('#ev-end').value;
|
||||
if(!title){ toast('Введите название'); $('#ev-name').focus(); return; }
|
||||
if(!date){ toast('Выберите дату'); return; }
|
||||
if(!start||!end){ toast('Укажите время начала и конца'); return; }
|
||||
if(start>=end){ toast('Конец должен быть позже начала'); return; }
|
||||
const payload={ title, date, start, end, ownerId:$('#ev-owner').value, category:$('#ev-cat').value };
|
||||
if(editingEventId){ Object.assign(data.events.find(x=>x.id===editingEventId), payload); toast('Сохранено'); }
|
||||
else { data.events.push(Object.assign({id:uid()}, payload)); toast('Событие добавлено 📅'); }
|
||||
selDay = date;
|
||||
save(); $('#event-modal').classList.add('hidden'); render();
|
||||
};
|
||||
$('#ev-delete').onclick = ()=>{
|
||||
if(!editingEventId) return;
|
||||
if(!confirm('Удалить событие?')) return;
|
||||
data.events = data.events.filter(x=>x.id!==editingEventId);
|
||||
save(); $('#event-modal').classList.add('hidden'); render();
|
||||
toast('Событие удалено');
|
||||
};
|
||||
|
||||
/* ---------- Семья ---------- */
|
||||
function renderFamily(){
|
||||
const cards = data.members.map(m=>{
|
||||
@ -401,6 +552,9 @@ function openTaskModal(id){
|
||||
$('#f-date').value = t?.dueDate||'';
|
||||
$('#f-time').value = t?.time||'';
|
||||
$('#f-priority').value = t?.priority||'medium';
|
||||
$('#f-duration').value = t?.duration||'';
|
||||
$('#f-category').innerHTML = catOptions();
|
||||
$('#f-category').value = t?.category||'other';
|
||||
$('#f-from').textContent = member(me).name;
|
||||
// список исполнителей
|
||||
$('#f-assignee').innerHTML = data.members.map(m=>`<option value="${m.id}" ${t&&t.assigneeId===m.id?'selected':m.id===me&&!t?'selected':''}>${escapeHtml(m.name)}</option>`).join('');
|
||||
@ -428,7 +582,9 @@ $('#modal-save').onclick = ()=>{
|
||||
assigneeId,
|
||||
dueDate: $('#f-date').value || null,
|
||||
time: $('#f-time').value || null,
|
||||
duration: $('#f-duration').value ? Number($('#f-duration').value) : null,
|
||||
priority: $('#f-priority').value,
|
||||
category: $('#f-category').value,
|
||||
};
|
||||
if(editingId){
|
||||
const t = data.tasks.find(x=>x.id===editingId);
|
||||
|
||||
68
index.html
68
index.html
@ -120,6 +120,27 @@ window.addEventListener('error',function(e){
|
||||
<label class="block text-sm font-semibold mb-1">Время</label>
|
||||
<input id="f-time" type="time" class="field">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Длительность</label>
|
||||
<select id="f-duration" class="field">
|
||||
<option value="">— не указана —</option>
|
||||
<option value="15">15 мин</option>
|
||||
<option value="30">30 мин</option>
|
||||
<option value="45">45 мин</option>
|
||||
<option value="60">1 ч</option>
|
||||
<option value="90">1.5 ч</option>
|
||||
<option value="120">2 ч</option>
|
||||
<option value="180">3 ч</option>
|
||||
<option value="240">4 ч</option>
|
||||
<option value="360">6 ч</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Вид</label>
|
||||
<select id="f-category" class="field"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Важность</label>
|
||||
<select id="f-priority" class="field">
|
||||
@ -185,6 +206,53 @@ window.addEventListener('error',function(e){
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модалка личного события (расписание) -->
|
||||
<div id="event-modal" class="hidden fixed inset-0 z-40 bg-black/50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||||
<div class="bg-white w-full sm:max-w-lg sm:rounded-2xl rounded-t-2xl shadow-2xl max-h-[92vh] overflow-auto">
|
||||
<div class="sticky top-0 bg-white border-b border-line px-5 py-3 flex items-center justify-between">
|
||||
<h3 id="ev-title" class="font-bold text-lg">Событие в расписании</h3>
|
||||
<button id="ev-close" class="text-2xl leading-none text-muted hover:text-ink px-2">×</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Что и где *</label>
|
||||
<input id="ev-name" class="field" placeholder="Напр. Школа, Бассейн, Работа">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Дата *</label>
|
||||
<input id="ev-date" type="date" class="field">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Чьё расписание</label>
|
||||
<select id="ev-owner" class="field"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">С *</label>
|
||||
<input id="ev-start" type="time" class="field">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">До *</label>
|
||||
<input id="ev-end" type="time" class="field">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold mb-1">Вид / цвет</label>
|
||||
<select id="ev-cat" class="field"></select>
|
||||
</div>
|
||||
<div class="bg-line rounded-lg p-3 text-xs text-muted">
|
||||
Личные события помогают планировать день: школа, работа, тренировки. Они не зависят от задач и видны в расписании.
|
||||
</div>
|
||||
</div>
|
||||
<div class="sticky bottom-0 bg-white border-t border-line px-5 py-3 flex gap-2">
|
||||
<button id="ev-delete" class="hidden btn-danger">Удалить</button>
|
||||
<button id="ev-save" class="btn-primary flex-1">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тост -->
|
||||
<div id="toast" class="hidden fixed bottom-5 left-1/2 -translate-x-1/2 z-50 bg-ink text-white px-5 py-3 rounded-full shadow-2xl text-sm font-semibold"></div>
|
||||
|
||||
|
||||
14
style.css
14
style.css
@ -53,6 +53,8 @@ textarea.field{resize:vertical}
|
||||
.cal-day{min-height:96px;background:#fff;border:1px solid var(--line);border-radius:10px;padding:6px;overflow:hidden}
|
||||
.cal-day.off{background:#fafbfc;color:#c2c8d2}
|
||||
.cal-day.today{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-50) inset}
|
||||
.cal-day.selected{outline:2px solid var(--accent);outline-offset:-2px}
|
||||
.cal-day[data-day]{cursor:pointer}
|
||||
.cal-dnum{font-size:12px;font-weight:700;margin-bottom:4px}
|
||||
.cal-task{font-size:11px;font-weight:600;border-radius:5px;padding:2px 5px;margin-bottom:3px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
@ -61,6 +63,18 @@ textarea.field{resize:vertical}
|
||||
|
||||
.stat-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px}
|
||||
|
||||
/* сегмент-переключатель */
|
||||
.seg{border:none;background:none;padding:6px 12px;font-size:13px;font-weight:600;color:var(--muted);border-radius:7px;cursor:pointer;font-family:inherit}
|
||||
.seg.active{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
||||
|
||||
/* элементы расписания */
|
||||
.dot{width:10px;height:10px;border-radius:999px;flex-shrink:0}
|
||||
.time-badge{font-size:11px;font-weight:700;text-align:center;line-height:1.2;min-width:52px;padding:6px 4px;border-radius:8px;white-space:nowrap}
|
||||
.time-badge span{display:block;font-size:10px;font-weight:600}
|
||||
.day-row{transition:box-shadow .15s}
|
||||
.day-row:hover{box-shadow:0 4px 14px rgba(15,18,24,.08)}
|
||||
.day-row .done{text-decoration:line-through;opacity:.6}
|
||||
|
||||
.menu-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:.12s}
|
||||
.menu-row:hover{background:var(--line)}
|
||||
.menu-row.active{background:var(--accent-50)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user