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:
|
||||
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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1969,6 +1969,8 @@ function openSettingsModal() {
|
||||
{ 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 () => {
|
||||
@@ -2350,6 +2352,8 @@ function bindSettingsModal() {
|
||||
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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user