i18n: style the settings select + add a topbar lang toggle
Two issues addressed: 1. The /settings language <select> was unstyled — .settings-select and .settings-status classes didn't exist, so the dropdown rendered with full native browser chrome and clashed visually with the rest of the panel. Added a terminal-aesthetic select: transparent background, 1px var(--border), custom chevron via crossed linear-gradients, accent border on focus/hover. Disabled options (ES/FR/DE 'coming soon') render in --dim. 2. Added a compact EN | IT pill in the topbar next to the theme toggle, mirroring the .tone-toggle visual rhythm. Shown only when a user is signed in (admins skipped). Optimistic UI: clicking flips the pill immediately, PATCHes /api/settings/language, and reverts on failure. On /log specifically the page reloads so the user sees the localized version of the strategic log right away. The /settings dropdown still surfaces all five languages (with ES/FR/DE disabled) for visibility; the topbar pill keeps to the two active languages to stay compact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
50ac6b9366
commit
fb71854238
2 changed files with 107 additions and 0 deletions
|
|
@ -136,6 +136,35 @@ a:hover { text-decoration: underline; }
|
||||||
color: var(--bg);
|
color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Language toggle in the topbar — same visual rhythm as the tone
|
||||||
|
* toggle so the two controls read as a pair. Only EN and IT are
|
||||||
|
* visible here; the WIP languages (ES/FR/DE) live in /settings. */
|
||||||
|
.lang-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.lang-toggle button {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
border: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
text-transform: inherit;
|
||||||
|
}
|
||||||
|
.lang-toggle button + button { border-left: 1px solid var(--border); }
|
||||||
|
.lang-toggle button:hover { color: var(--accent); }
|
||||||
|
.lang-toggle[data-lang="en"] button[data-value="en"],
|
||||||
|
.lang-toggle[data-lang="it"] button[data-value="it"] {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -1020,6 +1049,46 @@ details[open] .pf-analysis__head-left::before { content: "▾ "; }
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal-aesthetic <select> used in the Settings page. Native
|
||||||
|
* browser chrome stripped; we render a small chevron via crossed
|
||||||
|
* linear-gradients so the control matches the rest of the panel. */
|
||||||
|
.settings-select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 4px 28px 4px 8px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, var(--dim) 50%),
|
||||||
|
linear-gradient(-45deg, transparent 50%, var(--dim) 50%);
|
||||||
|
background-position: calc(100% - 13px) 50%, calc(100% - 9px) 50%;
|
||||||
|
background-size: 5px 5px, 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transition: border-color 120ms ease-out, color 120ms ease-out;
|
||||||
|
}
|
||||||
|
.settings-select:hover,
|
||||||
|
.settings-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.settings-select option { color: var(--text); background: var(--surface); }
|
||||||
|
.settings-select option:disabled { color: var(--dim); }
|
||||||
|
|
||||||
|
.settings-status {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.settings-status:empty { display: none; }
|
||||||
|
|
||||||
/* Sections are <details> elements — collapsed by default to keep the
|
/* Sections are <details> elements — collapsed by default to keep the
|
||||||
settings page scannable. Click the summary to expand. */
|
settings page scannable. Click the summary to expand. */
|
||||||
.settings-section {
|
.settings-section {
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,34 @@
|
||||||
if (el && window.htmx) window.htmx.trigger(el, 'tone-changed');
|
if (el && window.htmx) window.htmx.trigger(el, 'tone-changed');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.cassandraSetLang = async function (newLang) {
|
||||||
|
var pill = document.getElementById('lang-toggle');
|
||||||
|
if (!pill) return;
|
||||||
|
var prev = pill.dataset.lang;
|
||||||
|
if (prev === newLang) return;
|
||||||
|
// Optimistic update — flip the pill immediately so the click feels
|
||||||
|
// responsive. Revert on PATCH failure.
|
||||||
|
pill.dataset.lang = newLang;
|
||||||
|
try {
|
||||||
|
var r = await fetch('/api/settings/language', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({lang: newLang}),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
|
// Reload localized panels so the user immediately sees content
|
||||||
|
// in the new language (strategic log, dashboard header, etc.).
|
||||||
|
if (window.location.pathname === '/log' ||
|
||||||
|
window.location.pathname.startsWith('/log/')) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
pill.dataset.lang = prev;
|
||||||
|
console.warn('language switch failed:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Render any <time datetime="..."> in the browser's local timezone.
|
// Render any <time datetime="..."> in the browser's local timezone.
|
||||||
|
|
@ -180,6 +208,16 @@
|
||||||
<span class="theme-toggle__label"></span>
|
<span class="theme-toggle__label"></span>
|
||||||
</button>
|
</button>
|
||||||
{% set cu = request.state.current_user if request.state and request.state.current_user is defined else None %}
|
{% set cu = request.state.current_user if request.state and request.state.current_user is defined else None %}
|
||||||
|
{% if cu and cu.user %}
|
||||||
|
<div id="lang-toggle" class="lang-toggle" data-lang="{{ cu.user.lang or 'en' }}"
|
||||||
|
role="group" aria-label="AI output language"
|
||||||
|
title="Language the AI uses for the log, digest and portfolio commentary">
|
||||||
|
<button type="button" data-value="en"
|
||||||
|
onclick="cassandraSetLang('en')">EN</button>
|
||||||
|
<button type="button" data-value="it"
|
||||||
|
onclick="cassandraSetLang('it')">IT</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if cu and (cu.user or cu.is_admin) %}
|
{% if cu and (cu.user or cu.is_admin) %}
|
||||||
<div class="user-menu">
|
<div class="user-menu">
|
||||||
<button type="button" id="user-menu-toggle" class="user-chip"
|
<button type="button" id="user-menu-toggle" class="user-chip"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue