Changeset 0.25.3 (#163)
This commit is contained in:
@@ -65,6 +65,8 @@
|
||||
--banner-bottom-height: 0px;
|
||||
--banner-bg: transparent;
|
||||
--banner-fg: transparent;
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ── Light Theme ─────────────────────────────── */
|
||||
|
||||
@@ -252,9 +252,9 @@ const Pages = {
|
||||
// ── User Settings (settings surface) ─────
|
||||
|
||||
async saveProfile() {
|
||||
const name = _val('settingsDisplayName');
|
||||
const name = _val('profileDisplayName');
|
||||
if (!name) { _toast('Display name is required', 'error'); return; }
|
||||
const ok = await _api('PUT', '/api/v1/users/me', { display_name: name });
|
||||
const ok = await _api('PUT', '/api/v1/profile', { display_name: name });
|
||||
if (ok) _toast('Profile saved', 'success');
|
||||
},
|
||||
|
||||
@@ -265,7 +265,7 @@ const Pages = {
|
||||
if (!current || !newPw) { _toast('All password fields are required', 'error'); return; }
|
||||
if (newPw !== confirm) { _toast('Passwords do not match', 'error'); return; }
|
||||
if (newPw.length < 8) { _toast('Password must be at least 8 characters', 'error'); return; }
|
||||
const ok = await _api('PUT', '/api/v1/auth/password', { current_password: current, new_password: newPw });
|
||||
const ok = await _api('POST', '/api/v1/profile/password', { current_password: current, new_password: newPw });
|
||||
if (ok) {
|
||||
_toast('Password changed', 'success');
|
||||
_val('settingsCurrentPw', '');
|
||||
|
||||
@@ -314,18 +314,26 @@ function _initWorkspaceResize() {
|
||||
},
|
||||
onStart(_clientPos) {
|
||||
// Kill transition first, force reflow, then measure.
|
||||
// This ensures startW reflects the final CSS value (not mid-animation),
|
||||
// and the first onMove won't animate since transition is already dead.
|
||||
// Do NOT set inline width here — getBoundingClientRect includes border
|
||||
// but style.width is content-box, so it causes a 1px jump on click.
|
||||
// Pin inline width immediately to prevent snap-back if
|
||||
// the open-transition is still animating.
|
||||
secondary.style.transition = 'none';
|
||||
void secondary.offsetWidth;
|
||||
const startW = secondary.getBoundingClientRect().width;
|
||||
return { startW };
|
||||
// #surface has transform:scale(z) from applyAppearance.
|
||||
// getBoundingClientRect returns post-transform visual px.
|
||||
// Divide by scale to recover CSS px for style.width.
|
||||
const surface = document.getElementById('surface');
|
||||
const m = surface?.style.transform?.match(/scale\(([^)]+)\)/);
|
||||
const scale = m ? parseFloat(m[1]) : 1;
|
||||
const startW = secondary.getBoundingClientRect().width / scale;
|
||||
secondary.style.width = startW + 'px';
|
||||
secondary.style.minWidth = startW + 'px';
|
||||
return { startW, scale };
|
||||
},
|
||||
onMove(delta, ctx) {
|
||||
// Handle is left of secondary — dragging left (negative delta) grows it
|
||||
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, ctx.startW - delta));
|
||||
// Handle is left of secondary — dragging left (negative delta) grows it.
|
||||
// Mouse delta is in viewport px; convert to CSS px in scaled space.
|
||||
const cssDelta = delta / ctx.scale;
|
||||
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, ctx.startW - cssDelta));
|
||||
secondary.style.width = newW + 'px';
|
||||
secondary.style.minWidth = newW + 'px';
|
||||
},
|
||||
|
||||
@@ -1383,7 +1383,9 @@ const UI = {
|
||||
},
|
||||
closeSettings() {
|
||||
const base = window.__BASE__ || '';
|
||||
window.location.href = base + '/';
|
||||
const returnURL = sessionStorage.getItem('sb_settings_return');
|
||||
sessionStorage.removeItem('sb_settings_return');
|
||||
window.location.href = returnURL || (base + '/');
|
||||
},
|
||||
|
||||
switchSettingsTab(tab) {
|
||||
@@ -1479,15 +1481,27 @@ const UI = {
|
||||
// which only loaded on chat/settings — editor surface was skipped.
|
||||
|
||||
applyAppearance(scale, msgFont) {
|
||||
const z = scale === 100 ? '' : scale / 100;
|
||||
// Zoom content areas — covers both chat and editor surfaces
|
||||
document.querySelectorAll(
|
||||
'.sidebar, .workspace-primary, .workspace-secondary, ' +
|
||||
'.modal-overlay, .admin-panel, ' +
|
||||
'.surface-editor, .surface-notes'
|
||||
).forEach(el => el.style.zoom = z);
|
||||
const splash = document.getElementById('splashGate');
|
||||
if (splash) splash.style.zoom = z;
|
||||
// Use transform:scale() on #surface — the prototype's proven approach.
|
||||
// Transform operates on the visual layer without affecting layout flow
|
||||
// of siblings (banners stay unscaled). Inverse dimensions provide the
|
||||
// right layout space so the scaled visual output fills the viewport:
|
||||
// At 80%: width=125%, height=125% of surface → scale(0.8) → visual 100%
|
||||
// At 150%: width=66.7%, height=66.7% of surface → scale(1.5) → visual 100%
|
||||
const surface = document.getElementById('surface');
|
||||
if (surface) {
|
||||
if (scale === 100) {
|
||||
surface.style.transform = '';
|
||||
surface.style.transformOrigin = '';
|
||||
surface.style.width = '';
|
||||
surface.style.height = '';
|
||||
} else {
|
||||
const z = scale / 100;
|
||||
surface.style.transform = `scale(${z})`;
|
||||
surface.style.transformOrigin = 'top left';
|
||||
surface.style.width = (10000 / scale) + '%';
|
||||
surface.style.height = `calc(var(--surface-h) * ${10000 / scale} / 100)`;
|
||||
}
|
||||
}
|
||||
document.documentElement.style.setProperty('--msg-font', msgFont + 'px');
|
||||
localStorage.setItem('cs-appearance', JSON.stringify({ scale, msgFont }));
|
||||
// Recheck tab overflow after layout settles
|
||||
@@ -1499,6 +1513,9 @@ const UI = {
|
||||
},
|
||||
|
||||
restoreAppearance() {
|
||||
// Clear any stale zoom from previous scaling approaches
|
||||
document.body.style.zoom = '';
|
||||
document.documentElement.style.zoom = '';
|
||||
try {
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
const scale = prefs.scale || 100;
|
||||
|
||||
@@ -1001,6 +1001,7 @@ function updateTabArrows(tabs) {
|
||||
|
||||
const Theme = {
|
||||
_key: 'switchboard_theme',
|
||||
_mqListener: null,
|
||||
|
||||
init() {
|
||||
this.set(localStorage.getItem(this._key) || 'system');
|
||||
@@ -1008,8 +1009,26 @@ const Theme = {
|
||||
|
||||
set(mode) {
|
||||
localStorage.setItem(this._key, mode);
|
||||
if (mode === 'system') document.documentElement.removeAttribute('data-theme');
|
||||
else document.documentElement.setAttribute('data-theme', mode);
|
||||
// Remove any prior system-preference listener
|
||||
if (this._mqListener) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this._mqListener);
|
||||
this._mqListener = null;
|
||||
}
|
||||
if (mode === 'system') {
|
||||
// Resolve current OS preference and apply it
|
||||
const resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', resolved);
|
||||
// Listen for OS preference changes while in system mode
|
||||
this._mqListener = (e) => {
|
||||
// Only react if still in system mode
|
||||
if (this.get() === 'system') {
|
||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this._mqListener);
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', mode);
|
||||
}
|
||||
},
|
||||
|
||||
get() {
|
||||
|
||||
@@ -172,36 +172,39 @@ Object.assign(UI, {
|
||||
},
|
||||
|
||||
initAppearance() {
|
||||
// Load saved prefs on startup
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
UI.applyAppearance(prefs.scale || 100, prefs.msgFont || 14);
|
||||
UI.applyTheme(prefs.theme || 'system');
|
||||
// Theme and appearance are already applied by the universal init
|
||||
// in base.html (Theme.init() + UI.restoreAppearance()). This method
|
||||
// only wires up the settings UI controls.
|
||||
|
||||
// Live preview on slider change
|
||||
// Scale slider: show value live, but apply zoom only on release to avoid
|
||||
// jarring layout jumps mid-drag (zoom changes the layout viewport).
|
||||
const scaleEl = document.getElementById('settingsScale');
|
||||
const msgFontEl = document.getElementById('settingsMsgFont');
|
||||
if (scaleEl) scaleEl.addEventListener('input', () => {
|
||||
const v = parseInt(scaleEl.value);
|
||||
document.getElementById('scaleValue').textContent = v + '%';
|
||||
UI.applyAppearance(v, parseInt(msgFontEl?.value || 14));
|
||||
});
|
||||
if (msgFontEl) msgFontEl.addEventListener('input', () => {
|
||||
const v = parseInt(msgFontEl.value);
|
||||
document.getElementById('msgFontValue').textContent = v + 'px';
|
||||
UI.applyAppearance(parseInt(scaleEl?.value || 100), v);
|
||||
});
|
||||
if (scaleEl) {
|
||||
scaleEl.addEventListener('input', () => {
|
||||
document.getElementById('scaleValue').textContent = parseInt(scaleEl.value) + '%';
|
||||
});
|
||||
scaleEl.addEventListener('change', () => {
|
||||
UI.applyAppearance(parseInt(scaleEl.value), parseInt(msgFontEl?.value || 14));
|
||||
});
|
||||
}
|
||||
if (msgFontEl) {
|
||||
msgFontEl.addEventListener('input', () => {
|
||||
document.getElementById('msgFontValue').textContent = parseInt(msgFontEl.value) + 'px';
|
||||
});
|
||||
msgFontEl.addEventListener('change', () => {
|
||||
UI.applyAppearance(parseInt(scaleEl?.value || 100), parseInt(msgFontEl.value));
|
||||
});
|
||||
}
|
||||
|
||||
// Theme toggle buttons
|
||||
document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const theme = btn.dataset.theme;
|
||||
UI.applyTheme(theme);
|
||||
UI.applyTheme(theme); // delegates to Theme.set() — single source of truth
|
||||
document.querySelectorAll('#themeToggle .toggle-btn').forEach(b =>
|
||||
b.classList.toggle('active', b === btn)
|
||||
);
|
||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
p.theme = theme;
|
||||
localStorage.setItem('cs-appearance', JSON.stringify(p));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,10 +227,12 @@ Object.assign(UI, {
|
||||
});
|
||||
|
||||
// System theme change listener (for "system" mode)
|
||||
// Note: Theme.set('system') registers its own listener, but this
|
||||
// additionally fires the theme.changed event for CM6 editors.
|
||||
UI._systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
UI._systemThemeQuery.addEventListener('change', () => {
|
||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
if (p.theme === 'system') UI.applyTheme('system');
|
||||
const mode = typeof Theme !== 'undefined' ? Theme.get() : 'system';
|
||||
if (mode === 'system') UI.applyTheme('system');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -236,17 +241,21 @@ Object.assign(UI, {
|
||||
* @param {'light'|'dark'|'system'} mode
|
||||
*/
|
||||
applyTheme(mode) {
|
||||
let resolved = mode;
|
||||
if (mode === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
if (resolved === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
// Delegate to Theme API which handles system-mode resolution and
|
||||
// listens for OS preference changes.
|
||||
if (typeof Theme !== 'undefined') {
|
||||
Theme.set(mode);
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
// Fallback if Theme not loaded — resolve manually
|
||||
let resolved = mode;
|
||||
if (mode === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', resolved);
|
||||
}
|
||||
// Publish event for CM6 editors and extensions
|
||||
if (typeof Events !== 'undefined' && Events.emit) {
|
||||
const resolved = typeof Theme !== 'undefined' ? Theme.resolved() : mode;
|
||||
Events.emit('theme.changed', { mode, resolved });
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user