diff --git a/webapp/app.js b/webapp/app.js index 5e59c46..0543b4d 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -520,17 +520,20 @@ dates.forEach(function (date) { const isToday = date === todayKey; const dayClass = "duty-timeline-day" + (isToday ? " duty-timeline-day--today" : ""); - const dateLabel = isToday ? "Сегодня, " + dateKeyToDDMM(date) : dateKeyToDDMM(date); + const dateLabel = isToday ? dateKeyToDDMM(date) : dateKeyToDDMM(date); + const dateCellHtml = isToday + ? "Сегодня" + escapeHtml(dateLabel) + "" + : "" + escapeHtml(dateLabel) + ""; const dayDuties = duties.filter(function (d) { return localDateString(new Date(d.start_at)) === date; }).sort(function (a, b) { return new Date(a.start_at) - new Date(b.start_at); }); let dayHtml = ""; dayDuties.forEach(function (d) { const start = new Date(d.start_at); const end = new Date(d.end_at); const isCurrent = isToday && start <= now && now < end; - dayHtml += "
" + escapeHtml(dateLabel) + "
" + dutyTimelineCardHtml(d, isCurrent) + "
"; + dayHtml += "
" + dateCellHtml + "
" + dutyTimelineCardHtml(d, isCurrent) + "
"; }); if (dayDuties.length === 0 && isToday) { - dayHtml += "
" + escapeHtml(dateLabel) + "
"; + dayHtml += "
" + dateCellHtml + "
"; } fullHtml += "
" + dayHtml + "
"; }); diff --git a/webapp/style.css b/webapp/style.css index 14befdc..99ca27e 100644 --- a/webapp/style.css +++ b/webapp/style.css @@ -9,6 +9,8 @@ --unavailable: #e0af68; --vacation: #7dcfff; --error: #f7768e; + --timeline-date-width: 3.6em; + --timeline-track-width: 10px; } /* Light theme: prefer Telegram themeParams (--tg-theme-*), fallback to Telegram-like palette */ @@ -285,11 +287,20 @@ body { vertical-align: middle; } -/* Timeline: dates left, cards right */ +/* Timeline: dates | track (line + dot) | cards */ .duty-list.duty-timeline { - border-left: 2px solid var(--muted); - padding-left: 0; - margin-left: 2px; + position: relative; +} + +.duty-list.duty-timeline::before { + content: ""; + position: absolute; + left: calc(var(--timeline-date-width) + var(--timeline-track-width) / 2 - 1px); + top: 0; + bottom: 0; + width: 2px; + background: var(--muted); + pointer-events: none; } .duty-timeline-day { @@ -302,25 +313,121 @@ body { .duty-timeline-row { display: grid; - grid-template-columns: 4.2em 1fr; - gap: 0 10px; + grid-template-columns: var(--timeline-date-width) var(--timeline-track-width) 1fr; + gap: 0 4px; align-items: start; margin-bottom: 8px; min-height: 1px; } .duty-timeline-date { + position: relative; font-size: 0.8rem; color: var(--muted); padding-top: 10px; + padding-bottom: 10px; flex-shrink: 0; + overflow: visible; +} + +.duty-timeline-date::before { + content: ""; + position: absolute; + left: 0; + bottom: 4px; + width: calc(100% + var(--timeline-track-width) / 2); + height: 2px; + background: linear-gradient( + to right, + color-mix(in srgb, var(--muted) 40%, transparent) 0%, + color-mix(in srgb, var(--muted) 40%, transparent) 50%, + var(--muted) 70%, + var(--muted) 100% + ); +} + +.duty-timeline-date::after { + content: ""; + position: absolute; + left: calc(100% + (var(--timeline-track-width) / 2) - 1px); + bottom: 2px; + width: 2px; + height: 6px; + background: var(--muted); } .duty-timeline-day--today .duty-timeline-date { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-top: 4px; color: var(--today); font-weight: 600; } +.duty-timeline-day--today .duty-timeline-date::before, +.duty-timeline-day--today .duty-timeline-date::after { + display: none; +} + +.duty-timeline-date-label, +.duty-timeline-date-day { + display: block; + line-height: 1.25; +} + +.duty-timeline-date-day { + align-self: flex-start; + text-align: left; + padding-left: 0; + margin-left: 0; +} + +.duty-timeline-date-dot { + display: block; + width: 100%; + height: 8px; + min-height: 8px; + position: relative; + flex-shrink: 0; +} + +.duty-timeline-date-dot::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + margin-top: -1px; + width: calc(100% + var(--timeline-track-width) / 2); + height: 1px; + background: color-mix(in srgb, var(--today) 45%, transparent); +} + +.duty-timeline-date-dot::after { + content: ""; + position: absolute; + left: calc(100% + (var(--timeline-track-width) / 2) - 1px); + top: 50%; + margin-top: -3px; + width: 2px; + height: 6px; + background: var(--today); +} + +.duty-timeline-day--today .duty-timeline-date .duty-timeline-date-label { + color: var(--today); +} + +.duty-timeline-day--today .duty-timeline-date .duty-timeline-date-day { + color: var(--muted); + font-weight: 400; + font-size: 0.75rem; +} + +.duty-timeline-track { + min-width: 0; +} + .duty-timeline-card-wrap { min-width: 0; }