Einstellungen: Vollbild-Seite, Kontrast, Stundenhöhe, KW-Anzeige

- Einstellungen von Modal-Popup auf Vollbild-Seite mit Seitennavigation umgestellt
- Schriftkontrast (4 Stufen) und Linienkontrast (4 Stufen) pro Benutzer gespeichert
- Stundenhöhe (40/60/80/100px) in Wochen-/Tagesansicht per Einstellung steuerbar
- Kalenderwoche in Monats- und Wochenansicht grösser dargestellt
- CSS-Variable --hour-h für dynamische Zeitraster-Höhe in week.js und app.css
- Backend: neue Felder text_contrast, line_contrast, hour_height in UserSettings
This commit is contained in:
2026-03-27 10:43:39 +01:00
parent 2128f07037
commit c849f77651
8 changed files with 328 additions and 130 deletions

View File

@@ -43,6 +43,21 @@ def _migrate():
logging.info("Migration: added sidebar_hidden to google_calendars")
except Exception:
pass
try:
conn.execute(text("ALTER TABLE user_settings ADD COLUMN text_contrast INTEGER DEFAULT 3"))
conn.commit()
except Exception:
pass
try:
conn.execute(text("ALTER TABLE user_settings ADD COLUMN line_contrast INTEGER DEFAULT 3"))
conn.commit()
except Exception:
pass
try:
conn.execute(text("ALTER TABLE user_settings ADD COLUMN hour_height INTEGER DEFAULT 60"))
conn.commit()
except Exception:
pass
_migrate()

View File

@@ -75,6 +75,9 @@ class UserSettings(Base):
accent_color = Column(String(7), default="#ea4335")
today_color = Column(String(7), default="#4285f4")
dim_past_events = Column(Boolean, default=False)
text_contrast = Column(Integer, default=3)
line_contrast = Column(Integer, default=3)
hour_height = Column(Integer, default=60)
user = relationship("User", back_populates="settings")

View File

@@ -18,6 +18,9 @@ class SettingsUpdate(BaseModel):
accent_color: Optional[str] = None
today_color: Optional[str] = None
dim_past_events: Optional[bool] = None
text_contrast: Optional[int] = None
line_contrast: Optional[int] = None
hour_height: Optional[int] = None
def _settings_dict(s: models.UserSettings) -> dict:
@@ -28,6 +31,9 @@ def _settings_dict(s: models.UserSettings) -> dict:
"accent_color": s.accent_color,
"today_color": s.today_color,
"dim_past_events": s.dim_past_events,
"text_contrast": s.text_contrast or 3,
"line_contrast": s.line_contrast or 3,
"hour_height": s.hour_height or 60,
}

View File

@@ -440,12 +440,12 @@ a { color: var(--primary); text-decoration: none; }
/* ── Month View ─────────────────────────────────────────── */
.month-view { display: flex; flex-direction: column; height: 100%; }
.month-header {
display: grid; grid-template-columns: 32px repeat(7, 1fr);
display: grid; grid-template-columns: 38px repeat(7, 1fr);
border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.month-kw-header {
padding: 8px 0; text-align: center;
font-size: 10px; font-weight: 600; text-transform: uppercase;
font-size: 13px; font-weight: 700; text-transform: uppercase;
letter-spacing: .3px; color: var(--text-3);
border-right: 1px solid var(--border-light);
}
@@ -455,7 +455,7 @@ a { color: var(--primary); text-decoration: none; }
letter-spacing: .5px; color: var(--text-2);
}
.month-grid {
display: grid; grid-template-columns: 32px repeat(7, 1fr);
display: grid; grid-template-columns: 38px repeat(7, 1fr);
grid-template-rows: repeat(6, 1fr);
flex: 1; overflow: hidden;
}
@@ -464,7 +464,7 @@ a { color: var(--primary); text-decoration: none; }
border-bottom: 1px solid var(--border);
display: flex; align-items: flex-start; justify-content: center;
padding-top: 6px;
font-size: 10px; color: var(--text-3); font-weight: 500;
font-size: 13px; color: var(--text-3); font-weight: 700;
cursor: default; user-select: none;
min-height: 0;
}
@@ -516,7 +516,7 @@ a { color: var(--primary); text-decoration: none; }
}
/* KW badge in week view header gutter */
.week-kw-badge {
font-size: 10px; font-weight: 600; color: var(--text-3);
font-size: 14px; font-weight: 700; color: var(--text-3);
display: flex; align-items: flex-end; justify-content: center;
padding-bottom: 6px;
text-transform: uppercase; letter-spacing: .3px;
@@ -563,7 +563,7 @@ a { color: var(--primary); text-decoration: none; }
.week-body { display: flex; flex: 1; overflow-y: auto; position: relative; }
.week-time-col { width: 56px; flex-shrink: 0; position: relative; }
.time-label {
height: 60px; display: flex; align-items: flex-start; justify-content: flex-end;
height: var(--hour-h, 60px); display: flex; align-items: flex-start; justify-content: flex-end;
padding-right: 8px; padding-top: 6px;
font-size: 10px; color: var(--text-3);
}
@@ -571,18 +571,18 @@ a { color: var(--primary); text-decoration: none; }
.week-day-col {
flex: 1; border-left: 1px solid var(--border);
position: relative;
min-height: calc(60px * 24);
min-height: calc(var(--hour-h, 60px) * 24);
}
.hour-line {
position: absolute; left: 0; right: 0;
border-top: 1px solid var(--border-light);
height: 60px;
height: var(--hour-h, 60px);
}
.hour-line:first-child { border-top: none; }
.half-line {
position: absolute; left: 0; right: 0;
border-top: 1px dashed var(--border-light);
top: 30px;
top: calc(var(--hour-h, 60px) / 2);
}
/* Current time indicator */
@@ -699,7 +699,78 @@ a { color: var(--primary); text-decoration: none; }
.popup-time, .popup-location, .popup-calendar { font-size: 13px; color: var(--text-2); margin-bottom: 6px; }
.popup-description { font-size: 13px; color: var(--text-1); margin-bottom: 6px; white-space: pre-wrap; }
/* ── Settings ───────────────────────────────────────────── */
/* ── Settings Page ──────────────────────────────────────── */
#modal-settings.modal-overlay {
align-items: stretch; justify-content: stretch; padding: 0; background: var(--bg-app);
}
.settings-page-card {
width: 100%; height: 100%;
display: flex; flex-direction: column;
background: var(--bg-app); overflow: hidden;
}
.settings-page-header {
display: flex; align-items: center; gap: 8px;
padding: 14px 20px; border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.settings-page-header h3 { font-size: 16px; font-weight: 600; color: var(--text-1); margin: 0; }
.settings-page-body {
display: flex; flex: 1; overflow: hidden;
}
.settings-nav {
width: 200px; flex-shrink: 0;
border-right: 1px solid var(--border);
padding: 12px 8px;
display: flex; flex-direction: column; gap: 2px;
}
.settings-nav-btn {
display: block; width: 100%; text-align: left;
padding: 9px 14px; border-radius: var(--radius-sm);
font-size: 14px; color: var(--text-2);
background: none; border: none; cursor: pointer;
transition: background .15s, color .15s;
}
.settings-nav-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.settings-nav-btn.active { background: var(--primary-dim); color: var(--primary); font-weight: 600; }
.settings-panels {
flex: 1; overflow-y: auto; padding: 24px 28px;
}
.settings-panel { display: none; }
.settings-panel.active { display: block; }
/* Panel typography */
.panel-title {
font-size: 14px; font-weight: 600; color: var(--text-1); margin: 0 0 4px;
}
.panel-desc {
font-size: 12px; color: var(--text-3); margin: 0 0 12px;
}
/* Contrast / option selectors */
.contrast-selector {
display: flex; gap: 8px; flex-wrap: wrap;
}
.contrast-btn {
display: flex; flex-direction: column; align-items: center; gap: 6px;
padding: 10px 16px; border-radius: var(--radius);
border: 1px solid var(--border); background: var(--bg-surface);
cursor: pointer; transition: border-color .15s, background .15s;
min-width: 70px;
}
.contrast-btn:hover { border-color: var(--primary); }
.contrast-btn.active { border-color: var(--primary); background: var(--primary-dim); }
.contrast-btn span { font-size: 18px; font-weight: 700; line-height: 1; }
.contrast-lbl { font-size: 11px; color: var(--text-2); white-space: nowrap; }
.line-preview {
display: block; width: 36px; height: 0;
border-top: 2px solid; border-radius: 1px;
margin: 6px 0;
}
.hour-preview {
font-size: 14px; line-height: 1; color: var(--text-2);
}
/* ── Settings (legacy) ──────────────────────────────────── */
.settings-section { margin-bottom: 28px; }
.settings-section h4 { font-size: 14px; font-weight: 600; color: var(--text-1); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.badge-admin {

View File

@@ -378,96 +378,143 @@
</div>
</div>
<!-- Settings Modal -->
<!-- Settings Page -->
<div id="modal-settings" class="modal-overlay hidden">
<div class="modal-card" style="max-width:520px">
<div class="modal-header">
<div class="settings-page-card">
<div class="settings-page-header">
<button class="icon-btn modal-close" data-modal="modal-settings" style="margin-right:8px">
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h3>Einstellungen</h3>
<button class="icon-btn modal-close" data-modal="modal-settings">&times;</button>
<button class="btn btn-primary" id="settings-save" style="margin-left:auto">Speichern</button>
</div>
<div class="modal-body">
<div class="settings-section">
<h4>Darstellung</h4>
<div class="form-group">
<label>Standardansicht</label>
<select id="cfg-default-view">
<option value="month">Monat</option>
<option value="week">Woche</option>
<option value="day">Tag</option>
<option value="agenda">Termine</option>
</select>
</div>
<div class="form-group">
<label>Erster Wochentag</label>
<select id="cfg-week-start">
<option value="monday">Montag</option>
<option value="sunday">Sonntag</option>
</select>
</div>
<div class="form-group">
<label>Primärfarbe</label>
<div class="ev-color-row">
<input type="text" id="cfg-primary-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-primary-preview" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label>Akzentfarbe</label>
<div class="ev-color-row">
<input type="text" id="cfg-accent-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-accent-preview" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label>Heutige-Tag-Farbe</label>
<div class="ev-color-row">
<input type="text" id="cfg-today-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-today-preview" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="cfg-dim-past" />
Vergangene Termine ausgrauen
</label>
</div>
</div>
<div class="settings-page-body">
<nav class="settings-nav">
<button class="settings-nav-btn active" data-panel="appearance">Darstellung</button>
<button class="settings-nav-btn" data-panel="view">Ansicht &amp; Raster</button>
<button class="settings-nav-btn" data-panel="google">Google Konten</button>
<button class="settings-nav-btn" data-panel="hidden">Ausgeblendete Kalender</button>
<button class="settings-nav-btn hidden" data-panel="users" id="settings-nav-users">Benutzerverwaltung</button>
</nav>
<div class="settings-section" id="settings-google-section">
<h4>Google Konten</h4>
<div id="google-accounts-list"><span style="font-size:13px;color:var(--text-3)">Keine Google-Konten verbunden</span></div>
</div>
<div class="settings-panels">
<div class="settings-section" id="settings-hidden-cals-section">
<h4>Ausgeblendete Kalender</h4>
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div>
</div>
<div class="settings-section" id="settings-users-section">
<h4>Benutzerverwaltung <span class="badge-admin">Admin</span></h4>
<div id="users-list"></div>
<button class="btn btn-secondary" id="btn-add-user">Benutzer hinzufügen</button>
<div id="add-user-form" class="hidden" style="margin-top:12px">
<!-- Darstellung -->
<div class="settings-panel active" id="settings-panel-appearance">
<h4 class="panel-title">Farben</h4>
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="new-username" />
<label>Primärfarbe</label>
<div class="ev-color-row">
<input type="text" id="cfg-primary-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-primary-preview" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="new-password" />
<label>Akzentfarbe</label>
<div class="ev-color-row">
<input type="text" id="cfg-accent-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-accent-preview" title="Farbe wählen"></div>
</div>
</div>
<div class="form-group">
<label>Heutige-Tag-Farbe</label>
<div class="ev-color-row">
<input type="text" id="cfg-today-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
<div class="ev-color-preview" id="cfg-today-preview" title="Farbe wählen"></div>
</div>
</div>
<h4 class="panel-title" style="margin-top:24px">Schriftkontrast</h4>
<p class="panel-desc">Helligkeit der Beschriftungen und Texte</p>
<div class="contrast-selector" id="cfg-text-contrast" data-setting="text_contrast">
<button class="contrast-btn" data-val="1"><span style="color:#606070">Aa</span><span class="contrast-lbl">Dunkel</span></button>
<button class="contrast-btn" data-val="2"><span style="color:#9090a8">Aa</span><span class="contrast-lbl">Mittel</span></button>
<button class="contrast-btn" data-val="3"><span style="color:#c8c8d8">Aa</span><span class="contrast-lbl">Hell</span></button>
<button class="contrast-btn" data-val="4"><span style="color:#ffffff">Aa</span><span class="contrast-lbl">Maximum</span></button>
</div>
<h4 class="panel-title" style="margin-top:24px">Linienkontrast</h4>
<p class="panel-desc">Sichtbarkeit von Trennlinien und Rahmen</p>
<div class="contrast-selector" id="cfg-line-contrast" data-setting="line_contrast">
<button class="contrast-btn" data-val="1"><span class="line-preview" style="border-color:#1e1e2c"></span><span class="contrast-lbl">Kaum</span></button>
<button class="contrast-btn" data-val="2"><span class="line-preview" style="border-color:#2a2a3c"></span><span class="contrast-lbl">Subtil</span></button>
<button class="contrast-btn" data-val="3"><span class="line-preview" style="border-color:#3a3a52"></span><span class="contrast-lbl">Normal</span></button>
<button class="contrast-btn" data-val="4"><span class="line-preview" style="border-color:#5a5a78"></span><span class="contrast-lbl">Stark</span></button>
</div>
<label class="toggle-label" style="margin-bottom:8px">
<input type="checkbox" id="new-is-admin" /> Administrator
</label>
<button class="btn btn-primary" id="new-user-save">Erstellen</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" data-modal="modal-settings">Schließen</button>
<button class="btn btn-primary" id="settings-save">Speichern</button>
</div>
</div>
<!-- Ansicht & Raster -->
<div class="settings-panel" id="settings-panel-view">
<h4 class="panel-title">Kalenderansicht</h4>
<div class="form-group">
<label>Standardansicht</label>
<select id="cfg-default-view">
<option value="month">Monat</option>
<option value="week">Woche</option>
<option value="day">Tag</option>
<option value="agenda">Termine</option>
</select>
</div>
<div class="form-group">
<label>Erster Wochentag</label>
<select id="cfg-week-start">
<option value="monday">Montag</option>
<option value="sunday">Sonntag</option>
</select>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="cfg-dim-past" />
Vergangene Termine ausgrauen
</label>
</div>
<h4 class="panel-title" style="margin-top:24px">Stundenhöhe (Wochen- &amp; Tagesansicht)</h4>
<p class="panel-desc">Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt</p>
<div class="contrast-selector" id="cfg-hour-height" data-setting="hour_height">
<button class="contrast-btn" data-val="40"><span class="hour-preview">━━</span><span class="contrast-lbl">Kompakt</span></button>
<button class="contrast-btn" data-val="60"><span class="hour-preview">━━━</span><span class="contrast-lbl">Normal</span></button>
<button class="contrast-btn" data-val="80"><span class="hour-preview">━━━━</span><span class="contrast-lbl">Komfort</span></button>
<button class="contrast-btn" data-val="100"><span class="hour-preview">━━━━━</span><span class="contrast-lbl">Gross</span></button>
</div>
</div>
<!-- Google Konten -->
<div class="settings-panel" id="settings-panel-google">
<h4 class="panel-title">Google Konten</h4>
<div id="google-accounts-list"><span style="font-size:13px;color:var(--text-3)">Keine Google-Konten verbunden</span></div>
</div>
<!-- Ausgeblendete Kalender -->
<div class="settings-panel" id="settings-panel-hidden">
<h4 class="panel-title">Ausgeblendete Kalender</h4>
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div>
</div>
<!-- Benutzerverwaltung -->
<div class="settings-panel" id="settings-panel-users">
<h4 class="panel-title">Benutzerverwaltung <span class="badge-admin">Admin</span></h4>
<div id="users-list"></div>
<button class="btn btn-secondary" id="btn-add-user" style="margin-top:12px">Benutzer hinzufügen</button>
<div id="add-user-form" class="hidden" style="margin-top:12px">
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="new-username" />
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="new-password" />
</div>
<label class="toggle-label" style="margin-bottom:8px">
<input type="checkbox" id="new-is-admin" /> Administrator
</label>
<button class="btn btn-primary" id="new-user-save">Erstellen</button>
</div>
</div>
</div><!-- settings-panels -->
</div><!-- settings-page-body -->
</div><!-- settings-page-card -->
</div>
<!-- Profile Modal -->

View File

@@ -132,14 +132,16 @@ function renderView() {
},
showEventPopup,
false,
weekStartDay
weekStartDay,
state.settings.hour_height || 60
);
} else if (state.currentView === 'day') {
renderWeek(container, state.currentDate, evs,
(date, switchDay) => { if (!switchDay) openNewEventModal(date); },
showEventPopup,
true,
weekStartDay
weekStartDay,
state.settings.hour_height || 60
);
} else {
renderAgenda(container, state.currentDate, evs, showEventPopup);
@@ -1043,25 +1045,41 @@ function openSettingsModal() {
});
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
// Show users section only for admins
// Set active contrast/hour-height buttons
[
{ id: 'cfg-text-contrast', val: s.text_contrast || 3 },
{ id: 'cfg-line-contrast', val: s.line_contrast || 3 },
{ id: 'cfg-hour-height', val: s.hour_height || 60 },
].forEach(({ id, val }) => {
const sel = document.getElementById(id);
if (!sel) return;
sel.querySelectorAll('.contrast-btn').forEach(btn => {
btn.classList.toggle('active', String(btn.dataset.val) === String(val));
});
});
// Show users nav button only for admins
const user = JSON.parse(localStorage.getItem('user') || '{}');
const usersSection = document.getElementById('settings-users-section');
if (user.is_admin) {
usersSection.classList.remove('hidden');
loadUsers();
} else {
usersSection.classList.add('hidden');
}
const usersNavBtn = document.getElementById('settings-nav-users');
if (usersNavBtn) usersNavBtn.classList.toggle('hidden', !user.is_admin);
if (user.is_admin) loadUsers();
// Render Google accounts
// Activate first panel
const firstBtn = document.querySelector('.settings-nav-btn:not(.hidden)');
if (firstBtn) activateSettingsPanel(firstBtn.dataset.panel);
// Render Google accounts and hidden calendars
renderGoogleAccounts();
// Render hidden calendars
renderHiddenCalendars();
openModal('modal-settings');
}
function activateSettingsPanel(panel) {
document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.toggle('active', b.dataset.panel === panel));
document.querySelectorAll('.settings-panel').forEach(p => p.classList.toggle('active', p.id === 'settings-panel-' + panel));
}
function renderGoogleAccounts() {
const list = document.getElementById('google-accounts-list');
if (!list) return;
@@ -1203,6 +1221,21 @@ function bindSettingsModal() {
});
});
// Panel navigation
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
});
// Contrast / hour-height selectors
document.querySelectorAll('.contrast-selector').forEach(sel => {
sel.addEventListener('click', e => {
const btn = e.target.closest('.contrast-btn');
if (!btn) return;
sel.querySelectorAll('.contrast-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
document.getElementById('btn-add-user').onclick = () => {
document.getElementById('add-user-form').classList.toggle('hidden');
};
@@ -1223,6 +1256,10 @@ function bindSettingsModal() {
};
document.getElementById('settings-save').onclick = async () => {
const getActive = (id) => {
const btn = document.querySelector(`#${id} .contrast-btn.active`);
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,
@@ -1230,13 +1267,16 @@ function bindSettingsModal() {
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') || 60,
};
try {
await api.put('/settings/', settings);
state.settings = { ...state.settings, ...settings };
state.dimPast = settings.dim_past_events;
weekStartDay = settings.week_start_day;
applyTheme(settings);
applyTheme(state.settings);
showToast('Einstellungen gespeichert');
closeModal('modal-settings');
renderMiniCal();

View File

@@ -63,12 +63,37 @@ export function getISOWeekNumber(d) {
return Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
}
const TEXT_CONTRAST = {
1: { t1: '#606070', t2: '#484858', t3: '#303040' },
2: { t1: '#9090a8', t2: '#6a6a80', t3: '#484860' },
3: { t1: '#c8c8d8', t2: '#9090aa', t3: '#55556a' },
4: { t1: '#ffffff', t2: '#c0c0d8', t3: '#8888a0' },
};
const LINE_CONTRAST = {
1: { border: '#1e1e2c', light: '#181826' },
2: { border: '#2a2a3c', light: '#222230' },
3: { border: '#3a3a52', light: '#2e2e40' },
4: { border: '#5a5a78', light: '#484860' },
};
export function applyTheme(settings) {
const root = document.documentElement;
root.style.setProperty('--primary', settings.primary_color || '#4285f4');
root.style.setProperty('--primary-dim', hexToRgba(settings.primary_color || '#4285f4', 0.15));
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
root.style.setProperty('--primary', settings.primary_color || '#4285f4');
root.style.setProperty('--primary-dim', hexToRgba(settings.primary_color || '#4285f4', 0.15));
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
const tc = TEXT_CONTRAST[settings.text_contrast || 3];
root.style.setProperty('--text-1', tc.t1);
root.style.setProperty('--text-2', tc.t2);
root.style.setProperty('--text-3', tc.t3);
const lc = LINE_CONTRAST[settings.line_contrast || 3];
root.style.setProperty('--border', lc.border);
root.style.setProperty('--border-light', lc.light);
const hh = settings.hour_height || 60;
root.style.setProperty('--hour-h', hh + 'px');
}
function hexToRgba(hex, alpha) {

View File

@@ -2,7 +2,7 @@ import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../util
const DOW_SHORT = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday') {
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday', hourH = 60) {
// Build the days array (7 days for week, 1 for day)
const days = [];
if (isSingleDay) {
@@ -58,7 +58,6 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
).join('');
// ── Day columns ───────────────────────────────────────
// For each day, lay out timed events
const dayCols = days.map(day => {
const key = dayKey(day);
const dayEvs = timedEvs.filter(ev => {
@@ -66,18 +65,17 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
return isSameDay(s, day);
});
// Compute layout columns for overlapping events
const positioned = layoutEvents(dayEvs);
const hourLines = Array.from({length: 24}, (_, h) =>
`<div class="hour-line" style="top:${h * 60}px"><div class="half-line"></div></div>`
`<div class="hour-line" style="top:${h * hourH}px"><div class="half-line"></div></div>`
).join('');
const evHtml = positioned.map(({ ev, col, cols }) => {
const s = new Date(ev.start);
const e = new Date(ev.end);
const top = (s.getHours() * 60 + s.getMinutes());
const height = Math.max(20, (e - s) / 60000);
const top = s.getHours() * hourH + s.getMinutes() * hourH / 60;
const height = Math.max(20, (e - s) / 60000 * hourH / 60);
const left = (col / cols) * 100;
const width = (1 / cols) * 100 - 0.5;
const color = ev.color || ev.calendarColor || '#4285f4';
@@ -93,7 +91,7 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
</div>`;
}).join('');
return `<div class="week-day-col" data-date="${key}" style="height:${60*24}px">
return `<div class="week-day-col" data-date="${key}" style="height:${hourH * 24}px">
${hourLines}
${evHtml}
</div>`;
@@ -118,10 +116,10 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
// Scroll to ~8:00
const body = container.querySelector('.week-body');
if (body) body.scrollTop = 8 * 60 - 20;
if (body) body.scrollTop = 8 * hourH - 20;
// Render current-time line
renderNowLine(container, days);
renderNowLine(container, days, hourH);
// Click: slot
container.querySelectorAll('.week-day-col').forEach(col => {
@@ -129,9 +127,8 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
if (e.target.closest('.timed-event')) return;
const rect = col.getBoundingClientRect();
const y = e.clientY - rect.top + (container.querySelector('.week-body')?.scrollTop || 0);
const mins = Math.floor(y);
const h = Math.floor(mins / 60);
const m = Math.round((mins % 60) / 15) * 15;
const h = Math.floor(y / hourH);
const m = Math.round(((y % hourH) / hourH * 60) / 15) * 15;
const date = new Date(col.dataset.date + 'T00:00:00');
date.setHours(h, m, 0, 0);
onSlotClick(date);
@@ -164,34 +161,31 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
});
}
function renderNowLine(container, days) {
function renderNowLine(container, days, hourH = 60) {
const now = new Date();
const todayCol = container.querySelector(`.week-day-col[data-date="${dayKey(now)}"]`);
if (!todayCol) return;
const top = now.getHours() * 60 + now.getMinutes();
const top = now.getHours() * hourH + now.getMinutes() * hourH / 60;
const line = document.createElement('div');
line.className = 'now-line';
line.style.top = top + 'px';
line.innerHTML = '<div class="now-dot"></div>';
todayCol.appendChild(line);
// Update every minute
setTimeout(() => renderNowLine(container, days), 60000);
setTimeout(() => renderNowLine(container, days, hourH), 60000);
}
function layoutEvents(events) {
if (!events.length) return [];
// Sort by start time
const sorted = events.slice().sort((a, b) => new Date(a.start) - new Date(b.start));
const columns = []; // each column is an array of events
const columns = [];
const result = sorted.map(ev => {
const start = new Date(ev.start);
const end = new Date(ev.end);
// Find the first column where the event doesn't overlap
let placed = false;
for (let c = 0; c < columns.length; c++) {
const lastInCol = columns[c][columns[c].length - 1];
@@ -209,11 +203,8 @@ function layoutEvents(events) {
return ev;
});
// Calculate how many columns each event spans
return result.map(ev => {
const start = new Date(ev.start);
const end = new Date(ev.end);
// Count overlapping events
let maxCol = ev._col;
sorted.forEach(other => {
if (other === ev) return;