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 += "";
}
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;
}