diff --git a/backend/api.py b/backend/api.py index 960d814..a058b7f 100644 --- a/backend/api.py +++ b/backend/api.py @@ -115,7 +115,8 @@ async def create_item( start_time=event.start_time, duration_min=event.duration_min, title=event.title, - repeat_weekly=event.repeat_weekly + repeat_weekly=event.repeat_weekly, + color=event.color or "blue" ) if event.repeat_weekly: new_event.weekday = get_weekday_from_date(event.date) @@ -214,7 +215,8 @@ async def update_item( replacement_title=update.title or event.title, replacement_date=replacement_date, replacement_start_time=replacement_start_time, - replacement_duration_min=replacement_duration + replacement_duration_min=replacement_duration, + replacement_color=update.color or event.color )) await db.commit() return {"success": True, "action": "exception_created"} @@ -237,6 +239,8 @@ async def update_item( event.start_time = update.start_time if update.duration_min: event.duration_min = update.duration_min + if update.color: + event.color = update.color await db.commit() return {"success": True} diff --git a/backend/database.py b/backend/database.py index e1ab0e2..8319f0b 100644 --- a/backend/database.py +++ b/backend/database.py @@ -25,7 +25,7 @@ class Task(Base): class Event(Base): __tablename__ = "events" - + id = Column(Integer, primary_key=True, index=True) date = Column(String, index=True) # YYYY-MM-DD start_time = Column(String) # HH:MM @@ -33,6 +33,7 @@ class Event(Base): title = Column(String, nullable=False) repeat_weekly = Column(Boolean, default=False) weekday = Column(Integer) # 0=Monday, 6=Sunday, только для weekly events + color = Column(String, default="blue") # ключ цвета из палитры Catppuccin Mocha class WeeklyTaskException(Base): @@ -58,6 +59,7 @@ class EventException(Base): replacement_date = Column(String) replacement_start_time = Column(String) replacement_duration_min = Column(Integer) + replacement_color = Column(String) class TelegramUser(Base): @@ -85,6 +87,9 @@ async def _run_migrations(conn): await conn.execute(text("ALTER TABLE events ADD COLUMN repeat_weekly BOOLEAN DEFAULT 0")) if "weekday" not in event_columns: await conn.execute(text("ALTER TABLE events ADD COLUMN weekday INTEGER")) + if "color" not in event_columns: + await conn.execute(text("ALTER TABLE events ADD COLUMN color TEXT DEFAULT 'blue'")) + await conn.execute(text("UPDATE events SET color = 'blue' WHERE color IS NULL")) exception_columns = table_columns.get("weekly_task_exceptions", set()) if "task_id" not in exception_columns: @@ -92,6 +97,10 @@ async def _run_migrations(conn): if "replacement_date" not in exception_columns: await conn.execute(text("ALTER TABLE weekly_task_exceptions ADD COLUMN replacement_date TEXT")) + event_exception_columns = table_columns.get("event_exceptions", set()) + if "replacement_color" not in event_exception_columns: + await conn.execute(text("ALTER TABLE event_exceptions ADD COLUMN replacement_color TEXT")) + async def _get_table_columns(conn): table_names = ["tasks", "events", "weekly_task_exceptions", "event_exceptions"] diff --git a/backend/models.py b/backend/models.py index 406dcde..a354399 100644 --- a/backend/models.py +++ b/backend/models.py @@ -12,6 +12,7 @@ class EventCreate(BaseModel): duration_min: int title: str repeat_weekly: bool = False + color: str = "blue" # ключ цвета из палитры Catppuccin Mocha class TaskResponse(BaseModel): id: int @@ -28,6 +29,7 @@ class EventResponse(BaseModel): title: str kind: Literal["event"] = "event" repeat_weekly: bool = False + color: str = "blue" class ScheduleItem(BaseModel): kind: Literal["task", "event"] @@ -38,6 +40,7 @@ class ScheduleItem(BaseModel): start_time: Optional[str] = None duration_min: Optional[int] = None repeat_weekly: Optional[bool] = None + color: Optional[str] = None class ScheduleResponse(BaseModel): items: list[ScheduleItem] @@ -49,3 +52,4 @@ class UpdateRequest(BaseModel): duration_min: Optional[int] = None occurrence_date: Optional[str] = None scope: Optional[Literal["one_date", "series"]] = None # для weekly tasks + color: Optional[str] = None diff --git a/backend/utils.py b/backend/utils.py index 6d39c11..3aa89cf 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -102,7 +102,8 @@ async def materialize_events( title=event.title, start_time=event.start_time, duration_min=event.duration_min, - repeat_weekly=False + repeat_weekly=False, + color=event.color or "blue" )) result = await db.execute(select(Event).where(Event.repeat_weekly == True)) @@ -146,7 +147,8 @@ async def materialize_events( title=exception.replacement_title or event.title, start_time=exception.replacement_start_time or event.start_time, duration_min=exception.replacement_duration_min or event.duration_min, - repeat_weekly=True + repeat_weekly=True, + color=exception.replacement_color or event.color or "blue" )) continue @@ -158,7 +160,8 @@ async def materialize_events( title=event.title, start_time=event.start_time, duration_min=event.duration_min, - repeat_weekly=True + repeat_weekly=True, + color=event.color or "blue" )) current_date += timedelta(days=1) diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 8e51da9..043616b 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -4,7 +4,7 @@ Админка расписания - +
@@ -15,7 +15,6 @@

Планируйте задачи и занятия на несколько недель вперед, меняйте серии целиком и переносите встречи прямо мышкой по сетке времени.

-
@@ -29,11 +28,7 @@
- -
- Задачи - Занятия - Цикличные +
@@ -55,6 +50,6 @@ - + diff --git a/frontend/admin/static/script.js b/frontend/admin/static/script.js index d9704a9..37ce2ae 100644 --- a/frontend/admin/static/script.js +++ b/frontend/admin/static/script.js @@ -7,6 +7,28 @@ const START_HOUR = 8; const END_HOUR = 20; const PIXELS_PER_MINUTE = 0.8; +const CATPPUCCIN_MOCHA = { + rosewater: '#f5e0dc', + flamingo: '#f2cdcd', + pink: '#f5c2e7', + mauve: '#cba6f7', + red: '#f38ba8', + maroon: '#eba0ac', + peach: '#fab387', + yellow: '#f9e2af', + green: '#a6e3a1', + teal: '#94e2d5', + sky: '#89dceb', + sapphire: '#74c7ec', + blue: '#89b4fa', + lavender: '#b4befe' +}; +const DEFAULT_EVENT_COLOR = 'blue'; + +function eventColorHex(colorKey) { + return CATPPUCCIN_MOCHA[colorKey] || CATPPUCCIN_MOCHA[DEFAULT_EVENT_COLOR]; +} + let dragOffsetY = 0; let currentRangeStart = null; let scheduleItems = []; @@ -60,6 +82,11 @@ function formatDayLabel(dateStr) { }; } +function timeToMinutes(time) { + const [hour, minute] = time.split(':').map(Number); + return hour * 60 + minute; +} + function calculateEndTime(startTime, durationMin) { const [hour, minute] = startTime.split(':').map(Number); const totalMinutes = hour * 60 + minute + durationMin; @@ -68,11 +95,6 @@ function calculateEndTime(startTime, durationMin) { return `${String(endHour).padStart(2, '0')}:${String(endMinute).padStart(2, '0')}`; } -function timeToMinutes(time) { - const [hour, minute] = time.split(':').map(Number); - return hour * 60 + minute; -} - function minutesToTime(totalMinutes) { const hour = Math.floor(totalMinutes / 60); const minute = totalMinutes % 60; @@ -181,6 +203,7 @@ function renderEventBlock(event) { const startMinutes = timeToMinutes(event.start_time) - START_HOUR * 60; const top = Math.max(0, startMinutes * PIXELS_PER_MINUTE); const height = Math.max(42, event.duration_min * PIXELS_PER_MINUTE); + const colorHex = eventColorHex(event.color); const endTime = calculateEndTime(event.start_time, event.duration_min); return `
-
${event.start_time} - ${endTime}
-
${escapeHtml(event.title)}
-
${event.repeat_weekly ? 'Цикличное занятие' : 'Разовое занятие'}
+
+ ${event.start_time}–${endTime} + ${escapeHtml(event.title)} +
@@ -202,6 +226,38 @@ function renderEventBlock(event) { `; } +function renderColorPicker(selectedColor) { + const current = selectedColor || DEFAULT_EVENT_COLOR; + const swatches = Object.entries(CATPPUCCIN_MOCHA).map(([key, hex]) => ` + + `).join(''); + return ` +
+ + +
${swatches}
+
+ `; +} + +function bindColorPicker() { + const input = document.getElementById('event-color'); + document.querySelectorAll('.color-swatch').forEach((swatch) => { + swatch.addEventListener('click', () => { + document.querySelectorAll('.color-swatch').forEach((el) => el.classList.remove('is-selected')); + swatch.classList.add('is-selected'); + input.value = swatch.dataset.color; + }); + }); +} + function bindInteractiveHandlers() { document.removeEventListener('click', clearActiveItems); document.addEventListener('click', clearActiveItems); @@ -450,6 +506,7 @@ function showEventModal(initial = {}, item = null) {
+ ${renderColorPicker(initial.color)} ${isEdit ? buildScopeSelector(item) : ''}
@@ -459,6 +516,7 @@ function showEventModal(initial = {}, item = null) { `); + bindColorPicker(); document.getElementById('cancel-event').addEventListener('click', closeModal); document.getElementById('event-form').addEventListener('submit', async (event) => { event.preventDefault(); @@ -467,7 +525,8 @@ function showEventModal(initial = {}, item = null) { start_time: document.getElementById('event-start-time').value, duration_min: Number(document.getElementById('event-duration').value), title: document.getElementById('event-title').value.trim(), - repeat_weekly: document.getElementById('event-repeat-weekly').checked + repeat_weekly: document.getElementById('event-repeat-weekly').checked, + color: document.getElementById('event-color').value }; if (isEdit) { diff --git a/frontend/admin/static/style.css b/frontend/admin/static/style.css index 36d3f84..ecd704d 100644 --- a/frontend/admin/static/style.css +++ b/frontend/admin/static/style.css @@ -125,7 +125,7 @@ body { border-radius: 24px; padding: 18px 22px; display: flex; - justify-content: space-between; + justify-content: center; align-items: center; gap: 16px; margin-bottom: 16px; @@ -144,30 +144,9 @@ body { font-weight: 700; } -.legend { +.week-navigation { flex-wrap: wrap; -} - -.legend-chip { - padding: 8px 12px; - border-radius: 999px; - font-size: 13px; - border: 1px solid transparent; -} - -.task-chip { - background: rgba(21, 128, 61, 0.12); - color: var(--task); -} - -.event-chip { - background: rgba(15, 118, 110, 0.12); - color: var(--event); -} - -.recurring-chip { - background: rgba(245, 158, 11, 0.16); - color: var(--brand-deep); + justify-content: center; } .hint-panel { @@ -340,32 +319,86 @@ body { left: 14px; right: 14px; border-radius: 18px; - padding: 12px 14px; - background: linear-gradient(160deg, rgba(15, 118, 110, 0.92), rgba(17, 94, 89, 0.84)); - color: white; - box-shadow: 0 12px 24px rgba(15, 118, 110, 0.22); + padding: 8px 14px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 6px; + background: var(--event-color, #89b4fa); + color: #1e1e2e; + box-shadow: 0 12px 24px rgba(30, 30, 46, 0.22); cursor: grab; user-select: none; transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease; } .event-block.recurring { - outline: 2px solid rgba(245, 158, 11, 0.55); + outline: 2px solid rgba(30, 30, 46, 0.35); } .event-block.is-active { transform: scale(1.01); - box-shadow: 0 18px 32px rgba(15, 118, 110, 0.28); + box-shadow: 0 18px 32px rgba(30, 30, 46, 0.3); } .event-block.dragging { opacity: 0.5; } +.event-line { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; +} + .event-time { font-size: 12px; - opacity: 0.82; - margin-bottom: 6px; + opacity: 0.75; + flex-shrink: 0; + white-space: nowrap; +} + +.event-block .event-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.event-block .item-action.edit { + background: rgba(30, 30, 46, 0.14); + color: #1e1e2e; +} + +.event-block .item-action.delete { + background: rgba(30, 30, 46, 0.14); + color: #1e1e2e; +} + +.color-swatch-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.color-swatch { + width: 32px; + height: 32px; + border-radius: 999px; + border: 2px solid rgba(123, 92, 62, 0.18); + cursor: pointer; + padding: 0; + transition: transform 0.15s ease, border-color 0.15s ease; +} + +.color-swatch:hover { + transform: scale(1.08); +} + +.color-swatch.is-selected { + border-color: #1e1e2e; + box-shadow: 0 0 0 2px rgba(30, 30, 46, 0.2); } .empty-state { @@ -502,6 +535,10 @@ body { align-items: stretch; } + .hero-actions { + justify-content: flex-start; + } + .form-grid { grid-template-columns: 1fr; } @@ -509,6 +546,84 @@ body { @media (max-width: 560px) { .timeline { - height: 520px; + height: 480px; + } + + .hero { + padding: 20px; + border-radius: 22px; + } + + .hero h1 { + font-size: clamp(26px, 8vw, 36px); + } + + .hero-copy { + font-size: 15px; + } + + .eyebrow { + font-size: 11px; + } + + .hero-actions { + flex-direction: column; + } + + .hero-actions .btn { + width: 100%; + } + + .toolbar { + padding: 14px 16px; + } + + .week-navigation { + flex-direction: column; + gap: 10px; + } + + .week-navigation .nav-btn, + .week-navigation .btn { + width: 100%; + } + + .hint-panel { + font-size: 12px; + padding: 12px 14px; + flex-direction: column; + gap: 6px; + } + + .schedule-grid { + gap: 10px; + } + + .day-card { + padding: 12px; + border-radius: 20px; + min-height: 700px; + } + + .modal { + padding: 12px; + } + + .modal-content { + padding: 20px; + border-radius: 22px; + } + + .modal-title { + font-size: 24px; + } + + .form-grid { + gap: 10px; + } + + .color-swatch { + width: 28px; + height: 28px; } } diff --git a/frontend/public/index.html b/frontend/public/index.html index 93a397a..87055bf 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -4,7 +4,7 @@ Расписание - +
@@ -14,7 +14,12 @@

Задачи отображаются сверху, занятия ниже по времени. Данные обновляются автоматически каждые пять минут.

-
+ + +

Сегодня

@@ -31,6 +36,17 @@
- + + + diff --git a/frontend/public/static/script.js b/frontend/public/static/script.js index 4fb34ab..2fdac65 100644 --- a/frontend/public/static/script.js +++ b/frontend/public/static/script.js @@ -1,6 +1,34 @@ const MOSCOW_TIMEZONE = 'Europe/Moscow'; const WEEKDAYS = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье']; const MONTHS = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']; +const SLOT_MINUTES = 15; +const START_HOUR = 8; +const END_HOUR = 20; +const PIXELS_PER_MINUTE = 0.8; + +const CATPPUCCIN_MOCHA = { + rosewater: '#f5e0dc', + flamingo: '#f2cdcd', + pink: '#f5c2e7', + mauve: '#cba6f7', + red: '#f38ba8', + maroon: '#eba0ac', + peach: '#fab387', + yellow: '#f9e2af', + green: '#a6e3a1', + teal: '#94e2d5', + sky: '#89dceb', + sapphire: '#74c7ec', + blue: '#89b4fa', + lavender: '#b4befe' +}; +const DEFAULT_EVENT_COLOR = 'blue'; + +function eventColorHex(colorKey) { + return CATPPUCCIN_MOCHA[colorKey] || CATPPUCCIN_MOCHA[DEFAULT_EVENT_COLOR]; +} + +let currentWeekStart = null; function getMoscowDateString() { const formatter = new Intl.DateTimeFormat('en-CA', { @@ -27,6 +55,19 @@ function addDays(dateStr, days) { return formatISODate(date); } +function startOfWeek(dateStr = null) { + const base = parseISODate(dateStr || getMoscowDateString()); + const jsDay = base.getDay(); + const diff = jsDay === 0 ? -6 : 1 - jsDay; + base.setDate(base.getDate() + diff); + return formatISODate(base); +} + +function timeToMinutes(time) { + const [hour, minute] = time.split(':').map(Number); + return hour * 60 + minute; +} + function formatDate(dateStr) { const date = parseISODate(dateStr); return `${WEEKDAYS[(date.getDay() + 6) % 7]}, ${date.getDate()} ${MONTHS[date.getMonth()]}`; @@ -68,7 +109,7 @@ function renderDay(container, titleEl, dateStr, items, options = {}) { const eventsHtml = events.length ? `
${events.map((event) => ` -
+
${event.start_time}-${calculateEndTime(event.start_time, event.duration_min)} ${escapeHtml(event.title)}
@@ -114,5 +155,105 @@ async function loadSchedule() { ); } +function getWeekDates() { + return Array.from({ length: 7 }, (_, index) => addDays(currentWeekStart, index)); +} + +function renderTimeline(dateStr, events) { + const hours = Array.from({ length: END_HOUR - START_HOUR + 1 }, (_, index) => START_HOUR + index); + return ` +
+ ${hours.map((hour, index) => ` +
+ ${String(hour).padStart(2, '0')}:00 +
+ `).join('')} + ${events.map((event) => renderEventBlock(event)).join('')} +
+ `; +} + +function renderEventBlock(event) { + const startMinutes = timeToMinutes(event.start_time) - START_HOUR * 60; + const top = Math.max(0, startMinutes * PIXELS_PER_MINUTE); + const height = Math.max(42, event.duration_min * PIXELS_PER_MINUTE); + const endTime = calculateEndTime(event.start_time, event.duration_min); + return ` +
+
+ ${event.start_time}–${endTime} + ${escapeHtml(event.title)} +
+
+ `; +} + +async function loadWeekSchedule() { + if (!currentWeekStart) { + currentWeekStart = startOfWeek(); + } + + const dates = getWeekDates(); + const fromDate = dates[0]; + const toDate = dates[dates.length - 1]; + const today = getMoscowDateString(); + + const response = await fetch(`/api/schedule?from_date=${fromDate}&to_date=${toDate}`); + if (!response.ok) { + throw new Error('Не удалось загрузить расписание'); + } + + const data = await response.json(); + const grid = document.getElementById('week-grid'); + + grid.innerHTML = dates.map((dateStr) => { + const dayItems = data.items.filter((item) => item.date === dateStr); + const tasks = dayItems.filter((item) => item.kind === 'task'); + const events = dayItems + .filter((item) => item.kind === 'event') + .sort((a, b) => a.start_time.localeCompare(b.start_time)); + + return ` +
+
${formatDate(dateStr)}
+
+ ${tasks.map((task) => `
${escapeHtml(task.title)}
`).join('')} +
+ ${renderTimeline(dateStr, events)} +
+ `; + }).join(''); + + document.getElementById('week-range').textContent = `${formatDate(dates[0])} - ${formatDate(dates[dates.length - 1])}`; +} + +function setActiveTab(tab) { + const isWeek = tab === 'week'; + document.getElementById('view-daily').hidden = isWeek; + document.getElementById('view-week').hidden = !isWeek; + document.getElementById('tab-daily').classList.toggle('is-active', !isWeek); + document.getElementById('tab-week').classList.toggle('is-active', isWeek); +} + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('tab-daily').addEventListener('click', () => setActiveTab('daily')); + document.getElementById('tab-week').addEventListener('click', async () => { + setActiveTab('week'); + try { + await loadWeekSchedule(); + } catch (error) { + console.error(error); + } + }); + document.getElementById('week-prev').addEventListener('click', async () => { + currentWeekStart = addDays(currentWeekStart, -7); + await loadWeekSchedule().catch((error) => console.error(error)); + }); + document.getElementById('week-next').addEventListener('click', async () => { + currentWeekStart = addDays(currentWeekStart, 7); + await loadWeekSchedule().catch((error) => console.error(error)); + }); +}); + loadSchedule().catch((error) => console.error(error)); setInterval(() => loadSchedule().catch((error) => console.error(error)), 5 * 60 * 1000); diff --git a/frontend/public/static/style.css b/frontend/public/static/style.css index b944432..b76533d 100644 --- a/frontend/public/static/style.css +++ b/frontend/public/static/style.css @@ -65,12 +65,45 @@ body { line-height: 1.4; } +.view-tabs { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.view-tab { + border: 1px solid rgba(123, 92, 62, 0.18); + background: rgba(255, 251, 246, 0.75); + color: var(--muted); + border-radius: 999px; + padding: 10px 20px; + font-size: 15px; + font-family: inherit; + cursor: pointer; + transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease; +} + +.view-tab:hover { + transform: translateY(-1px); +} + +.view-tab.is-active { + background: linear-gradient(135deg, #d97706 0%, #9a3412 100%); + color: white; + border-color: transparent; +} + .days-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; } +.days-grid[hidden], +.week-view[hidden] { + display: none; +} + .day-panel { border-radius: 28px; padding: 28px; @@ -115,23 +148,188 @@ body { display: grid; grid-template-columns: 160px 1fr; gap: 14px; - background: rgba(15, 118, 110, 0.08); - border: 1px solid rgba(15, 118, 110, 0.14); + background: color-mix(in srgb, var(--event-color, #89b4fa) 16%, white); + border: 1px solid var(--event-color, #89b4fa); + border-left: 8px solid var(--event-color, #89b4fa); font-size: 28px; align-items: start; } .event-time { - color: var(--event); + color: #1f2937; font-weight: 700; } +.event-title { + color: #1f2937; +} + .empty-message { color: var(--muted); border: 1px dashed var(--line); font-size: 24px; } +.week-view { + background: var(--panel); + border: 1px solid rgba(255, 255, 255, 0.6); + box-shadow: 0 18px 45px rgba(101, 67, 33, 0.12); + backdrop-filter: blur(14px); + border-radius: 28px; + padding: 24px; + margin-top: 20px; +} + +.week-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; + flex-wrap: wrap; +} + +.nav-btn { + border: 1px solid rgba(123, 92, 62, 0.18); + background: rgba(255, 251, 246, 0.9); + color: var(--text); + border-radius: 999px; + padding: 10px 16px; + font-size: 14px; + font-family: inherit; + cursor: pointer; + transition: transform 0.18s ease; +} + +.nav-btn:hover { + transform: translateY(-1px); +} + +.range-title { + font-size: 18px; + font-weight: 700; + text-align: center; +} + +.week-scroll { + overflow-x: auto; + padding-bottom: 8px; + scrollbar-width: thin; +} + +.week-grid { + display: grid; + grid-template-columns: repeat(7, minmax(200px, 1fr)); + gap: 14px; + min-width: 1500px; +} + +.week-day-card { + border-radius: 22px; + padding: 14px; + background: rgba(255, 255, 255, 0.5); + border: 1px solid var(--line); +} + +.week-day-card.is-today { + outline: 2px solid rgba(217, 119, 6, 0.28); +} + +.week-day-header { + font-size: 15px; + font-weight: 700; + margin-bottom: 10px; + min-height: 2.4em; + line-height: 1.2; +} + +.week-task-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 12px; +} + +.week-task-item { + background: rgba(22, 101, 52, 0.08); + border: 1px solid rgba(22, 101, 52, 0.14); + border-radius: 12px; + padding: 8px 10px; + font-size: 13px; +} + +.timeline { + position: relative; + height: 576px; + border-radius: 18px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(252, 246, 238, 0.95) 100%); + border: 1px solid var(--line); + overflow: hidden; +} + +.timeline-hour { + position: absolute; + left: 0; + right: 0; + height: 48px; + border-top: 1px solid rgba(123, 92, 62, 0.12); +} + +.timeline-hour-label { + position: absolute; + top: -8px; + left: 8px; + font-size: 10px; + color: var(--muted); + background: rgba(255, 250, 242, 0.9); + padding: 0 4px; +} + +.event-block { + position: absolute; + left: 8px; + right: 8px; + border-radius: 14px; + padding: 6px 10px; + display: flex; + align-items: center; + background: var(--event-color, #89b4fa); + color: #1e1e2e; + box-shadow: 0 10px 20px rgba(30, 30, 46, 0.2); + overflow: hidden; +} + +.event-block.recurring { + outline: 2px solid rgba(30, 30, 46, 0.35); +} + +.event-block .event-line { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + width: 100%; +} + +.event-block .event-time { + font-size: 11px; + opacity: 0.75; + flex-shrink: 0; + white-space: nowrap; + color: #1e1e2e; +} + +.event-block .event-title { + font-size: 12px; + font-weight: 700; + line-height: 1.2; + color: #1e1e2e; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + @media (max-width: 900px) { .days-grid { grid-template-columns: 1fr; @@ -151,3 +349,80 @@ body { grid-template-columns: 1fr; } } + +@media (max-width: 640px) { + body { + padding: 14px; + } + + .board-hero { + padding: 20px; + border-radius: 22px; + } + + .board-hero h1 { + font-size: clamp(26px, 8vw, 38px); + } + + .board-label { + font-size: 11px; + } + + .board-copy { + font-size: 16px; + } + + .view-tabs { + gap: 8px; + } + + .view-tab { + flex: 1; + padding: 10px 12px; + font-size: 14px; + text-align: center; + } + + .day-panel { + padding: 18px; + border-radius: 20px; + } + + .day-title { + font-size: 22px; + } + + .task-item, + .event-item { + font-size: 16px; + padding: 12px 14px; + } + + .event-item { + gap: 6px; + } + + .week-view { + padding: 16px; + border-radius: 20px; + } + + .week-toolbar { + justify-content: center; + text-align: center; + } + + .range-title { + width: 100%; + order: -1; + margin-bottom: 4px; + } + + .timeline { + height: 480px; + } + + .week-day-card { + padding: 10px; + } +}