feat: Monatswechsel-Markierung in Monatsansicht

In der rolling Monatsansicht wird jetzt am Monatswechsel:
- eine dickere Trennlinie gezeichnet (links bei Wechsel mitten in Zeile,
  oben bei Zeilenstart)
- das 3-Buchstaben-Monatskürzel (z.B. JUL, AUG) groß über der "1"
  angezeigt

Beide Farben (Linie und Kürzel) sind in den Einstellungen unter
"Farben" individuell anpassbar (Default: #7090c0).

Backend: neue UserSettings-Felder month_divider_color und month_label_color
mit Migration. Frontend: applyTheme setzt entsprechende CSS-Variablen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guido Schmit
2026-05-09 16:49:52 +02:00
parent 15b6c90b11
commit 006c1f994c
9 changed files with 97 additions and 17 deletions

View File

@@ -97,6 +97,18 @@ def _migrate():
except Exception:
pass
try:
conn.execute(text("ALTER TABLE user_settings ADD COLUMN month_divider_color VARCHAR(7) DEFAULT '#7090c0'"))
conn.commit()
except Exception:
pass
try:
conn.execute(text("ALTER TABLE user_settings ADD COLUMN month_label_color VARCHAR(7) DEFAULT '#7090c0'"))
conn.commit()
except Exception:
pass
_migrate()
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)

View File

@@ -82,6 +82,8 @@ class UserSettings(Base):
line_contrast = Column(Integer, default=3)
hour_height = Column(Integer, default=60)
language = Column(String(5), default="de")
month_divider_color = Column(String(7), default="#7090c0")
month_label_color = Column(String(7), default="#7090c0")
user = relationship("User", back_populates="settings")

View File

@@ -22,6 +22,8 @@ class SettingsUpdate(BaseModel):
line_contrast: Optional[int] = None
hour_height: Optional[int] = None
language: Optional[str] = None
month_divider_color: Optional[str] = None
month_label_color: Optional[str] = None
def _settings_dict(s: models.UserSettings) -> dict:
@@ -36,6 +38,8 @@ def _settings_dict(s: models.UserSettings) -> dict:
"line_contrast": s.line_contrast or 3,
"hour_height": s.hour_height or 60,
"language": s.language or "de",
"month_divider_color": s.month_divider_color or "#7090c0",
"month_label_color": s.month_label_color or "#7090c0",
}

View File

@@ -497,6 +497,30 @@ a { color: var(--primary); text-decoration: none; }
border-radius: 50%; flex-shrink: 0;
}
.cell-day.today { background: var(--today-color); color: #fff; font-weight: 700; }
/* Month boundary marker: thicker line + month abbreviation on the 1st */
.month-col.first-of-month {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.month-col.month-divider-left {
box-shadow: inset 3px 0 0 0 var(--month-divider-color, #7090c0);
}
.month-row.month-divider-top {
box-shadow: inset 0 3px 0 0 var(--month-divider-color, #7090c0);
}
.month-marker {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .5px;
color: var(--month-label-color, #7090c0);
line-height: 1;
padding: 0 2px;
margin-bottom: -2px;
}
/* Events overlay — pointer-events:none so clicks pass to columns */
.month-events-overlay {
position: absolute; top: 30px; left: 0; right: 0; bottom: 0;

View File

@@ -603,6 +603,20 @@
<div class="ev-color-preview" id="cfg-today-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label data-i18n="settings_month_divider_color">Monatswechsel-Linie</label>
<div class="ev-color-row">
<input type="text" id="cfg-month-divider-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-month-divider-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label data-i18n="settings_month_label_color">Monatskürzel</label>
<div class="ev-color-row">
<input type="text" id="cfg-month-label-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-month-label-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
</div>
</div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_text_contrast">Schriftkontrast</h4>
<p class="panel-desc" data-i18n="settings_text_contrast_desc">Helligkeit der Beschriftungen und Texte</p>

View File

@@ -1966,9 +1966,11 @@ function openSettingsModal() {
document.getElementById('cfg-default-view').value = s.default_view || 'month';
document.getElementById('cfg-week-start').value = s.week_start_day || 'monday';
const colors = [
{ id: 'cfg-primary', val: s.primary_color || '#4285f4' },
{ id: 'cfg-accent', val: s.accent_color || '#ea4335' },
{ id: 'cfg-today', val: s.today_color || '#4285f4' },
{ id: 'cfg-primary', val: s.primary_color || '#4285f4' },
{ id: 'cfg-accent', val: s.accent_color || '#ea4335' },
{ id: 'cfg-today', val: s.today_color || '#4285f4' },
{ id: 'cfg-month-divider', val: s.month_divider_color || '#7090c0' },
{ id: 'cfg-month-label', val: s.month_label_color || '#7090c0' },
];
colors.forEach(({ id, val }) => {
document.getElementById(id + '-hex').value = val.toUpperCase();
@@ -2291,7 +2293,7 @@ async function loadUsers() {
}
function bindSettingsModal() {
['cfg-primary','cfg-accent','cfg-today'].forEach(prefix => {
['cfg-primary','cfg-accent','cfg-today','cfg-month-divider','cfg-month-label'].forEach(prefix => {
const preview = document.getElementById(prefix + '-preview');
const hex = document.getElementById(prefix + '-hex');
preview.addEventListener('click', async () => {
@@ -2345,16 +2347,18 @@ function bindSettingsModal() {
return btn ? Number(btn.dataset.val) : null;
};
const settings = {
default_view: document.getElementById('cfg-default-view').value,
week_start_day: document.getElementById('cfg-week-start').value,
primary_color: document.getElementById('cfg-primary-hex').value,
accent_color: document.getElementById('cfg-accent-hex').value,
today_color: document.getElementById('cfg-today-hex').value,
dim_past_events: document.getElementById('cfg-dim-past').checked,
text_contrast: getActive('cfg-text-contrast') || 3,
line_contrast: getActive('cfg-line-contrast') || 3,
hour_height: getActive('cfg-hour-height') || 44,
language: document.getElementById('cfg-language').value,
default_view: document.getElementById('cfg-default-view').value,
week_start_day: document.getElementById('cfg-week-start').value,
primary_color: document.getElementById('cfg-primary-hex').value,
accent_color: document.getElementById('cfg-accent-hex').value,
today_color: document.getElementById('cfg-today-hex').value,
month_divider_color: document.getElementById('cfg-month-divider-hex').value,
month_label_color: document.getElementById('cfg-month-label-hex').value,
dim_past_events: document.getElementById('cfg-dim-past').checked,
text_contrast: getActive('cfg-text-contrast') || 3,
line_contrast: getActive('cfg-line-contrast') || 3,
hour_height: getActive('cfg-hour-height') || 44,
language: document.getElementById('cfg-language').value,
};
try {
await api.put('/settings/', settings);

View File

@@ -65,6 +65,8 @@ const translations = {
settings_colors: 'Farben',
settings_primary_color: 'Primärfarbe', settings_accent_color: 'Akzentfarbe',
settings_today_color: 'Heutige-Tag-Farbe',
settings_month_divider_color: 'Monatswechsel-Linie',
settings_month_label_color: 'Monatskürzel-Farbe',
settings_text_contrast: 'Schriftkontrast',
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
@@ -274,6 +276,8 @@ const translations = {
settings_colors: 'Colors',
settings_primary_color: 'Primary color', settings_accent_color: 'Accent color',
settings_today_color: 'Today highlight color',
settings_month_divider_color: 'Month divider line',
settings_month_label_color: 'Month label color',
settings_text_contrast: 'Text contrast',
settings_text_contrast_desc: 'Brightness of labels and text',
contrast_dark: 'Dark', contrast_medium: 'Medium',

View File

@@ -94,6 +94,9 @@ export function applyTheme(settings) {
const hh = settings.hour_height || 44;
root.style.setProperty('--hour-h', hh + 'px');
root.style.setProperty('--month-divider-color', settings.month_divider_color || '#7090c0');
root.style.setProperty('--month-label-color', settings.month_label_color || '#7090c0');
}
function hexToRgba(hex, alpha) {

View File

@@ -121,8 +121,9 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
});
// Full-height column divs (click targets + borders)
const monthsShort = t('months_short');
let colsHtml = '';
rowCells.forEach(cell => {
rowCells.forEach((cell, idx) => {
const key = dateKey(cell);
const isOther = cell.getMonth() !== primaryMonth;
const todayCls = isToday(cell) ? 'today' : '';
@@ -130,12 +131,24 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
const selDate = selectedDate || currentDate;
const selectedCls = isSameDay(cell, selDate) ? 'month-selected' : '';
const numCls = isToday(cell) ? 'today' : '';
colsHtml += `<div class="month-col ${todayCls} ${otherCls} ${selectedCls}" data-date="${key}">
// First-of-month marker: show month abbreviation, push day number below
const isFirstOfMonth = cell.getDate() === 1;
const firstCls = isFirstOfMonth ? 'first-of-month' : '';
// Add divider class on the cell BEFORE a month change (for right border styling)
// and on the cell AT a month change (for left border styling) — except at row start
const dividerCls = (isFirstOfMonth && idx > 0) ? 'month-divider-left' : '';
const monthLabel = isFirstOfMonth
? `<div class="month-marker">${monthsShort[cell.getMonth()]}</div>`
: '';
colsHtml += `<div class="month-col ${todayCls} ${otherCls} ${selectedCls} ${firstCls} ${dividerCls}" data-date="${key}">
${monthLabel}
<div class="cell-day ${numCls}">${cell.getDate()}</div>
</div>`;
});
bodyHtml += `<div class="month-row">
// If the row starts on the 1st of a new month, draw a divider above the row
const rowDividerCls = rowCells[0].getDate() === 1 ? 'month-divider-top' : '';
bodyHtml += `<div class="month-row ${rowDividerCls}">
<div class="month-kw-cell">${kw}</div>
<div class="month-row-right">
${colsHtml}