Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion static/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2035,7 +2035,7 @@ async function loadMcpServers() {
${hasTools ? `<span style="font-size:10px;opacity:0.4;">Click to manage tools</span>` : ''}
</div>
<div style="display:flex;gap:4px;align-items:center;">
${s.needs_oauth ? `<a href="/api/mcp/oauth/authorize/${s.id}" target="_blank" class="admin-btn-sm" style="background:var(--red);color:#fff;text-decoration:none;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:600;">Authorize</a>` : ''}
${s.needs_oauth ? `<a href="/api/mcp/oauth/authorize/${s.id}" target="_blank" class="admin-btn-sm" style="background:var(--red);color:var(--on-accent);text-decoration:none;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:600;">Authorize</a>` : ''}
<button class="admin-btn-sm" data-adm-mcp-reconnect="${s.id}">Reconnect</button>
<button class="admin-btn-delete" style="border-color:${s.is_enabled ? 'color-mix(in srgb, var(--red) 30%, transparent)' : 'color-mix(in srgb, var(--fg) 30%, transparent)'};color:${s.is_enabled ? 'var(--red)' : 'var(--fg)'};" data-adm-mcp-toggle="${s.id}" data-adm-mcp-enable="${!s.is_enabled}">${s.is_enabled ? 'Disable' : 'Enable'}</button>
<button class="admin-btn-delete" data-adm-mcp-delete="${s.id}">Delete</button>
Expand Down
2 changes: 1 addition & 1 deletion static/js/compare/scoreboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function showScoreboard() {
confirmLabel.textContent = 'Clear all vote history?';
const yesBtn = document.createElement('button');
yesBtn.textContent = 'Clear';
yesBtn.style.cssText = 'padding:4px 12px;background:var(--red);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600;';
yesBtn.style.cssText = 'padding:4px 12px;background:var(--red);color:var(--on-accent);border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600;';
yesBtn.addEventListener('click', () => {
Storage.setJSON(VOTES_STORAGE_KEY, []);
overlay.remove();
Expand Down
2 changes: 1 addition & 1 deletion static/js/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -3838,7 +3838,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
</div>
<div class="modal-footer" style="display:flex;gap:8px;justify-content:flex-end;">
<button class="memory-toolbar-btn" id="att-warn-cancel">Go back</button>
<button class="memory-toolbar-btn" id="att-warn-send" style="background:var(--accent-primary,var(--red));color:#fff;border-color:var(--accent-primary,var(--red));">Send anyway</button>
<button class="memory-toolbar-btn" id="att-warn-send" style="background:var(--accent-primary,var(--red));color:var(--on-accent-primary);border-color:var(--accent-primary,var(--red));">Send anyway</button>
</div>
</div>
`;
Expand Down
4 changes: 2 additions & 2 deletions static/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2827,7 +2827,7 @@ async function initEmailAccountsSettings() {
<div class="settings-row eaf-smtp-creds"><label class="settings-label">Username${_hint('Usually the same as your IMAP username (your email address).')}</label><input id="eaf-smtp-user" class="settings-input" value="${esc(a.smtp_user || '')}"></div>
<div class="settings-row eaf-smtp-creds"><label class="settings-label">Password${_hint('Your SMTP password — often the same as your IMAP password. Outlook / Office 365 generally requires OAuth and will not work with a normal password here.')}</label><input id="eaf-smtp-pass" class="settings-input" type="password" placeholder="${isEdit && a.has_smtp_password ? '(unchanged)' : ''}"></div>
<div class="settings-row" style="margin-top:10px;align-items:center;">
<button class="admin-btn-add" id="eaf-save" style="background:var(--red);border-color:var(--red);color:#fff;display:inline-flex;align-items:center;gap:5px;font-weight:600;">
<button class="admin-btn-add" id="eaf-save" style="background:var(--red);border-color:var(--red);color:var(--on-accent);display:inline-flex;align-items:center;gap:5px;font-weight:600;">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
${isEdit ? 'Save' : 'Create'}
</button>
Expand Down Expand Up @@ -4439,7 +4439,7 @@ async function initUnifiedIntegrations() {
<div style="font-weight:600;margin-bottom:3px;">${esc(n.title)}</div>
<div style="opacity:0.8;margin-bottom:6px;">${esc(n.body)}</div>
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
<a href="${esc(n.url)}" target="_blank" rel="noopener noreferrer" class="admin-btn-sm" style="background:var(--red);border-color:var(--red);color:#fff;text-decoration:none;display:inline-flex;align-items:center;gap:5px;font-weight:600;">
<a href="${esc(n.url)}" target="_blank" rel="noopener noreferrer" class="admin-btn-sm" style="background:var(--red);border-color:var(--red);color:var(--on-accent);text-decoration:none;display:inline-flex;align-items:center;gap:5px;font-weight:600;">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
${esc(n.linkLabel || 'Generate App Password')}
</a>
Expand Down
61 changes: 61 additions & 0 deletions static/js/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,38 @@ function _syncCustomThemesToServer(ct) {
}

// --- Syntax color derivation from theme base colors ---
// WCAG relative luminance (0..1). Used to choose readable text for content
// painted on a --red background: --red is theme-configurable, and a light
// accent needs dark text where a darker one reads fine with white.
// HSL lightness is not a substitute -- #e06c75 and #f2c14e sit at nearly
// identical HSL-L but 0.27 vs 0.56 luminance, since the WCAG formula
// weights green (0.7152) far above red (0.2126) and blue (0.0722).
function _relativeLuminance(hex) {
const { r, g, b } = hexToRgb(hex) || { r: 0, g: 0, b: 0 };
const lin = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}

// Pick a readable foreground for an arbitrary background: white unless it
// would fall below ~3:1, in which case near-black. Shared by every
// accent-painted surface so each one is judged against the background it
// actually renders, not a global accent.
function _readableOn(bgHex) {
const contrastWithWhite = 1.05 / (_relativeLuminance(bgHex) + 0.05);
return contrastWithWhite < 3 ? '#171717' : '#fff';
}

// sRGB mix matching CSS color-mix(in srgb, a pct%, b). Needed where a
// surface's background is itself a mix, e.g. the new-chat send button.
function _mixSrgb(aHex, bHex, aPct) {
const a = hexToRgb(aHex) || { r: 0, g: 0, b: 0 };
const b = hexToRgb(bHex) || { r: 0, g: 0, b: 0 };
const w = Math.max(0, Math.min(1, aPct));
const ch = (x, y) => Math.round(x * w + y * (1 - w));
const hex = (n) => n.toString(16).padStart(2, '0');
return `#${hex(ch(a.r, b.r))}${hex(ch(a.g, b.g))}${hex(ch(a.b, b.b))}`;
}

function hexToHSL(hex) {
const rgb = hexToRgb(hex) || { r: 0, g: 0, b: 0 };
const r = rgb.r / 255;
Expand Down Expand Up @@ -261,6 +293,10 @@ export function applyColors(colors) {
s.setProperty('--panel', colors.panel);
s.setProperty('--border', colors.border);
if (colors.red) s.setProperty('--red', colors.red);
// Foreground for surfaces painted with --red itself. Surfaces with an
// independently configurable background get their own token below,
// once the advanced overrides have been resolved.
s.setProperty('--on-accent', _readableOn(colors.red || '#e06c75'));

// Keep the mobile browser toolbar / status bar matched to the theme bg
// (same as the early head-script does on first paint).
Expand All @@ -287,6 +323,31 @@ export function applyColors(colors) {
s.setProperty(css, adv[key] || defaults[key]);
}

// Primary-accent surfaces can be independently overridden from --red, so
// derive their foreground from the background they actually use.
const _accentPrimary = adv.accentPrimary || colors.red || '#e06c75';
s.setProperty('--accent-primary', _accentPrimary);
s.setProperty('--on-accent-primary', _readableOn(_accentPrimary));
const _accentPrimaryHover = _mixSrgb(_accentPrimary, '#ffffff', 0.85);
s.setProperty('--accent-primary-hover', _accentPrimaryHover);
s.setProperty('--on-accent-primary-hover', _readableOn(_accentPrimaryHover));

// The send button's background is independently configurable
// (--send-btn-bg / --send-btn-hover), so a foreground derived from
// --red can be wrong for it: a dark accent with a light custom send
// button would keep white text on a light surface. Pair each of its
// rendered backgrounds with its own foreground instead.
const _sendBg = adv.sendBtnBg || defaults.sendBtnBg;
const _sendHoverBg = adv.sendBtnHover || defaults.sendBtnHover;
s.setProperty('--on-send-btn', _readableOn(_sendBg));
s.setProperty('--on-send-btn-hover', _readableOn(_sendHoverBg));
// The new-chat state blends the hover colour 85% into --panel, so it
// is a third distinct surface.
s.setProperty(
'--on-send-btn-newchat-hover',
_readableOn(_mixSrgb(_sendHoverBg, colors.panel || '#111111', 0.85)),
);

// Update favicon to match theme accent color
_updateFavicon(colors.red || '#e06c75');
}
Expand Down
Loading