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.
This commit is contained in:
@@ -97,6 +97,18 @@ def _migrate():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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()
|
_migrate()
|
||||||
|
|
||||||
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ class UserSettings(Base):
|
|||||||
line_contrast = Column(Integer, default=3)
|
line_contrast = Column(Integer, default=3)
|
||||||
hour_height = Column(Integer, default=60)
|
hour_height = Column(Integer, default=60)
|
||||||
language = Column(String(5), default="de")
|
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")
|
user = relationship("User", back_populates="settings")
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class SettingsUpdate(BaseModel):
|
|||||||
line_contrast: Optional[int] = None
|
line_contrast: Optional[int] = None
|
||||||
hour_height: Optional[int] = None
|
hour_height: Optional[int] = None
|
||||||
language: Optional[str] = None
|
language: Optional[str] = None
|
||||||
|
month_divider_color: Optional[str] = None
|
||||||
|
month_label_color: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def _settings_dict(s: models.UserSettings) -> dict:
|
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,
|
"line_contrast": s.line_contrast or 3,
|
||||||
"hour_height": s.hour_height or 60,
|
"hour_height": s.hour_height or 60,
|
||||||
"language": s.language or "de",
|
"language": s.language or "de",
|
||||||
|
"month_divider_color": s.month_divider_color or "#7090c0",
|
||||||
|
"month_label_color": s.month_label_color or "#7090c0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -497,6 +497,30 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
border-radius: 50%; flex-shrink: 0;
|
border-radius: 50%; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.cell-day.today { background: var(--today-color); color: #fff; font-weight: 700; }
|
.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 */
|
/* Events overlay — pointer-events:none so clicks pass to columns */
|
||||||
.month-events-overlay {
|
.month-events-overlay {
|
||||||
position: absolute; top: 30px; left: 0; right: 0; bottom: 0;
|
position: absolute; top: 30px; left: 0; right: 0; bottom: 0;
|
||||||
|
|||||||
@@ -603,6 +603,20 @@
|
|||||||
<div class="ev-color-preview" id="cfg-today-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
|
<div class="ev-color-preview" id="cfg-today-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
|
||||||
</div>
|
</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>
|
<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>
|
<p class="panel-desc" data-i18n="settings_text_contrast_desc">Helligkeit der Beschriftungen und Texte</p>
|
||||||
|
|||||||
@@ -1966,9 +1966,11 @@ function openSettingsModal() {
|
|||||||
document.getElementById('cfg-default-view').value = s.default_view || 'month';
|
document.getElementById('cfg-default-view').value = s.default_view || 'month';
|
||||||
document.getElementById('cfg-week-start').value = s.week_start_day || 'monday';
|
document.getElementById('cfg-week-start').value = s.week_start_day || 'monday';
|
||||||
const colors = [
|
const colors = [
|
||||||
{ id: 'cfg-primary', val: s.primary_color || '#4285f4' },
|
{ id: 'cfg-primary', val: s.primary_color || '#4285f4' },
|
||||||
{ id: 'cfg-accent', val: s.accent_color || '#ea4335' },
|
{ id: 'cfg-accent', val: s.accent_color || '#ea4335' },
|
||||||
{ id: 'cfg-today', val: s.today_color || '#4285f4' },
|
{ 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 }) => {
|
colors.forEach(({ id, val }) => {
|
||||||
document.getElementById(id + '-hex').value = val.toUpperCase();
|
document.getElementById(id + '-hex').value = val.toUpperCase();
|
||||||
@@ -2291,7 +2293,7 @@ async function loadUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindSettingsModal() {
|
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 preview = document.getElementById(prefix + '-preview');
|
||||||
const hex = document.getElementById(prefix + '-hex');
|
const hex = document.getElementById(prefix + '-hex');
|
||||||
preview.addEventListener('click', async () => {
|
preview.addEventListener('click', async () => {
|
||||||
@@ -2345,16 +2347,18 @@ function bindSettingsModal() {
|
|||||||
return btn ? Number(btn.dataset.val) : null;
|
return btn ? Number(btn.dataset.val) : null;
|
||||||
};
|
};
|
||||||
const settings = {
|
const settings = {
|
||||||
default_view: document.getElementById('cfg-default-view').value,
|
default_view: document.getElementById('cfg-default-view').value,
|
||||||
week_start_day: document.getElementById('cfg-week-start').value,
|
week_start_day: document.getElementById('cfg-week-start').value,
|
||||||
primary_color: document.getElementById('cfg-primary-hex').value,
|
primary_color: document.getElementById('cfg-primary-hex').value,
|
||||||
accent_color: document.getElementById('cfg-accent-hex').value,
|
accent_color: document.getElementById('cfg-accent-hex').value,
|
||||||
today_color: document.getElementById('cfg-today-hex').value,
|
today_color: document.getElementById('cfg-today-hex').value,
|
||||||
dim_past_events: document.getElementById('cfg-dim-past').checked,
|
month_divider_color: document.getElementById('cfg-month-divider-hex').value,
|
||||||
text_contrast: getActive('cfg-text-contrast') || 3,
|
month_label_color: document.getElementById('cfg-month-label-hex').value,
|
||||||
line_contrast: getActive('cfg-line-contrast') || 3,
|
dim_past_events: document.getElementById('cfg-dim-past').checked,
|
||||||
hour_height: getActive('cfg-hour-height') || 44,
|
text_contrast: getActive('cfg-text-contrast') || 3,
|
||||||
language: document.getElementById('cfg-language').value,
|
line_contrast: getActive('cfg-line-contrast') || 3,
|
||||||
|
hour_height: getActive('cfg-hour-height') || 44,
|
||||||
|
language: document.getElementById('cfg-language').value,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await api.put('/settings/', settings);
|
await api.put('/settings/', settings);
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const translations = {
|
|||||||
settings_colors: 'Farben',
|
settings_colors: 'Farben',
|
||||||
settings_primary_color: 'Primärfarbe', settings_accent_color: 'Akzentfarbe',
|
settings_primary_color: 'Primärfarbe', settings_accent_color: 'Akzentfarbe',
|
||||||
settings_today_color: 'Heutige-Tag-Farbe',
|
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: 'Schriftkontrast',
|
||||||
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
||||||
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
|
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
|
||||||
@@ -274,6 +276,8 @@ const translations = {
|
|||||||
settings_colors: 'Colors',
|
settings_colors: 'Colors',
|
||||||
settings_primary_color: 'Primary color', settings_accent_color: 'Accent color',
|
settings_primary_color: 'Primary color', settings_accent_color: 'Accent color',
|
||||||
settings_today_color: 'Today highlight 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: 'Text contrast',
|
||||||
settings_text_contrast_desc: 'Brightness of labels and text',
|
settings_text_contrast_desc: 'Brightness of labels and text',
|
||||||
contrast_dark: 'Dark', contrast_medium: 'Medium',
|
contrast_dark: 'Dark', contrast_medium: 'Medium',
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ export function applyTheme(settings) {
|
|||||||
|
|
||||||
const hh = settings.hour_height || 44;
|
const hh = settings.hour_height || 44;
|
||||||
root.style.setProperty('--hour-h', hh + 'px');
|
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) {
|
function hexToRgba(hex, alpha) {
|
||||||
|
|||||||
@@ -121,8 +121,9 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Full-height column divs (click targets + borders)
|
// Full-height column divs (click targets + borders)
|
||||||
|
const monthsShort = t('months_short');
|
||||||
let colsHtml = '';
|
let colsHtml = '';
|
||||||
rowCells.forEach(cell => {
|
rowCells.forEach((cell, idx) => {
|
||||||
const key = dateKey(cell);
|
const key = dateKey(cell);
|
||||||
const isOther = cell.getMonth() !== primaryMonth;
|
const isOther = cell.getMonth() !== primaryMonth;
|
||||||
const todayCls = isToday(cell) ? 'today' : '';
|
const todayCls = isToday(cell) ? 'today' : '';
|
||||||
@@ -130,12 +131,24 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
|
|||||||
const selDate = selectedDate || currentDate;
|
const selDate = selectedDate || currentDate;
|
||||||
const selectedCls = isSameDay(cell, selDate) ? 'month-selected' : '';
|
const selectedCls = isSameDay(cell, selDate) ? 'month-selected' : '';
|
||||||
const numCls = isToday(cell) ? 'today' : '';
|
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 class="cell-day ${numCls}">${cell.getDate()}</div>
|
||||||
</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-kw-cell">${kw}</div>
|
||||||
<div class="month-row-right">
|
<div class="month-row-right">
|
||||||
${colsHtml}
|
${colsHtml}
|
||||||
|
|||||||
Reference in New Issue
Block a user