fix: theme toggle now properly handles auto mode
Remove data-theme attribute when mode="auto" instead of setting it to "auto". Pico's OS-dark selector is :root:not([data-theme]), so any value (even "auto") prevents dark-mode Pico styles. Add Pico form element background vars and color-scheme: light to match active theme. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+15
-1
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
Theme selectors mirror Pico's own so equal specificity + later source order
|
Theme selectors mirror Pico's own so equal specificity + later source order
|
||||||
wins: light = :root:not([data-theme=dark]) ; dark = the two dark selectors.
|
wins: light = :root:not([data-theme=dark]) ; dark = the two dark selectors.
|
||||||
`data-theme="auto"` is not `light`, so it follows the OS preference.
|
The "system" state renders with **no** data-theme attribute (base.html
|
||||||
|
removes it) — Pico's OS-dark block is `:root:not([data-theme])`, so a literal
|
||||||
|
data-theme="auto" would keep Pico light while this file went dark.
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/* --- Tokens: light (paper) ----------------------------------------------- */
|
/* --- Tokens: light (paper) ----------------------------------------------- */
|
||||||
@@ -73,9 +75,15 @@
|
|||||||
--pico-form-element-placeholder-color: var(--im-text-faint);
|
--pico-form-element-placeholder-color: var(--im-text-faint);
|
||||||
--pico-form-element-active-border-color: var(--im-accent);
|
--pico-form-element-active-border-color: var(--im-accent);
|
||||||
--pico-form-element-focus-color: var(--im-accent);
|
--pico-form-element-focus-color: var(--im-accent);
|
||||||
|
/* Pico swaps the *background* on :focus/:active. Remap it too, or a focused
|
||||||
|
field falls back to Pico's own palette instead of ours. */
|
||||||
|
--pico-form-element-active-background-color: var(--im-surface);
|
||||||
|
--pico-form-element-selected-background-color: var(--im-surface-3);
|
||||||
--pico-code-background-color: var(--im-surface-3);
|
--pico-code-background-color: var(--im-surface-3);
|
||||||
--pico-code-color: var(--im-text);
|
--pico-code-color: var(--im-text);
|
||||||
--pico-modal-overlay-background-color: rgba(24, 30, 36, .42);
|
--pico-modal-overlay-background-color: rgba(24, 30, 36, .42);
|
||||||
|
/* Native widgets: <select> popup, scrollbars, autofill. */
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Tokens: dark (ink) -------------------------------------------------- */
|
/* --- Tokens: dark (ink) -------------------------------------------------- */
|
||||||
@@ -140,9 +148,12 @@
|
|||||||
--pico-form-element-placeholder-color: var(--im-text-faint);
|
--pico-form-element-placeholder-color: var(--im-text-faint);
|
||||||
--pico-form-element-active-border-color: var(--im-accent);
|
--pico-form-element-active-border-color: var(--im-accent);
|
||||||
--pico-form-element-focus-color: var(--im-accent);
|
--pico-form-element-focus-color: var(--im-accent);
|
||||||
|
--pico-form-element-active-background-color: var(--im-surface-3);
|
||||||
|
--pico-form-element-selected-background-color: var(--im-surface-3);
|
||||||
--pico-code-background-color: var(--im-surface-3);
|
--pico-code-background-color: var(--im-surface-3);
|
||||||
--pico-code-color: var(--im-text);
|
--pico-code-color: var(--im-text);
|
||||||
--pico-modal-overlay-background-color: rgba(4, 8, 11, .62);
|
--pico-modal-overlay-background-color: rgba(4, 8, 11, .62);
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,9 +217,12 @@
|
|||||||
--pico-form-element-placeholder-color: var(--im-text-faint);
|
--pico-form-element-placeholder-color: var(--im-text-faint);
|
||||||
--pico-form-element-active-border-color: var(--im-accent);
|
--pico-form-element-active-border-color: var(--im-accent);
|
||||||
--pico-form-element-focus-color: var(--im-accent);
|
--pico-form-element-focus-color: var(--im-accent);
|
||||||
|
--pico-form-element-active-background-color: var(--im-surface-3);
|
||||||
|
--pico-form-element-selected-background-color: var(--im-surface-3);
|
||||||
--pico-code-background-color: var(--im-surface-3);
|
--pico-code-background-color: var(--im-surface-3);
|
||||||
--pico-code-color: var(--im-text);
|
--pico-code-color: var(--im-text);
|
||||||
--pico-modal-overlay-background-color: rgba(4, 8, 11, .62);
|
--pico-modal-overlay-background-color: rgba(4, 8, 11, .62);
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Typography + global rhythm ----------------------------------------- */
|
/* --- Typography + global rhythm ----------------------------------------- */
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{% import "partials/icons.html" as ico %}
|
{% import "partials/icons.html" as ico %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="auto">
|
{# No data-theme attribute in the "auto" state on purpose: Pico's OS-dark block
|
||||||
|
is keyed on `:root:not([data-theme])`, so any value at all — even "auto" —
|
||||||
|
pins Pico to its light theme while app.css switches to its dark tokens. That
|
||||||
|
mismatch is what produced white-on-white focused inputs. #}
|
||||||
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@@ -18,14 +22,23 @@
|
|||||||
dark: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z"/></svg>',
|
dark: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z"/></svg>',
|
||||||
auto: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="12" cy="12" r="8"/><path d="M12 4v16a8 8 0 0 0 0-16Z" fill="currentColor" stroke="none"/></svg>'
|
auto: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="12" cy="12" r="8"/><path d="M12 4v16a8 8 0 0 0 0-16Z" fill="currentColor" stroke="none"/></svg>'
|
||||||
},
|
},
|
||||||
|
// "auto" must *remove* the attribute, not set it to "auto" — see the
|
||||||
|
// comment on <html> above.
|
||||||
|
apply() {
|
||||||
|
if (this.current === 'auto') {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', this.current);
|
||||||
|
}
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
document.documentElement.setAttribute('data-theme', this.current);
|
this.apply();
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const order = ['light', 'dark', 'auto'];
|
const order = ['light', 'dark', 'auto'];
|
||||||
this.current = order[(order.indexOf(this.current) + 1) % order.length];
|
this.current = order[(order.indexOf(this.current) + 1) % order.length];
|
||||||
localStorage.setItem('imptune_theme', this.current);
|
localStorage.setItem('imptune_theme', this.current);
|
||||||
document.documentElement.setAttribute('data-theme', this.current);
|
this.apply();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
{% for item in driver_data %}
|
{% for item in driver_data %}
|
||||||
<option value="{{ item.driver.id }}"
|
<option value="{{ item.driver.id }}"
|
||||||
{% if p.driver_id == item.driver.id %}selected{% endif %}>
|
{% if p.driver_id == item.driver.id %}selected{% endif %}>
|
||||||
{{ item.driver.original_filename }}{% if item.names %} — {{ item.names | join(', ') }}{% endif %}
|
{{ item.driver.label }}{% if item.names %} — {{ item.names | join(', ') }}{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
~ (printers | map(attribute='ip_address') | join(' ')) ~ ' '
|
~ (printers | map(attribute='ip_address') | join(' ')) ~ ' '
|
||||||
~ (printers | map(attribute='port_name') | join(' ')) ~ ' '
|
~ (printers | map(attribute='port_name') | join(' ')) ~ ' '
|
||||||
~ (printers | selectattr('driver_id') | map(attribute='driver.original_filename') | join(' ')) ~ ' '
|
~ (printers | selectattr('driver_id') | map(attribute='driver.original_filename') | join(' ')) ~ ' '
|
||||||
|
~ (printers | selectattr('driver_id') | map(attribute='driver.label') | join(' ')) ~ ' '
|
||||||
~ client_name %}
|
~ client_name %}
|
||||||
<section class="card card-flush"
|
<section class="card card-flush"
|
||||||
data-search="{{ gsearch | lower }}"
|
data-search="{{ gsearch | lower }}"
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for p in printers %}
|
{% for p in printers %}
|
||||||
<tr data-search="{{ (p.name ~ ' ' ~ p.ip_address ~ ' ' ~ p.port_name ~ ' ' ~ client_name ~ ' ' ~ (p.driver.original_filename if p.driver_id else '')) | lower }}"
|
<tr data-search="{{ (p.name ~ ' ' ~ p.ip_address ~ ' ' ~ p.port_name ~ ' ' ~ client_name ~ ' ' ~ (p.driver.original_filename ~ ' ' ~ p.driver.label if p.driver_id else '')) | lower }}"
|
||||||
x-data
|
x-data
|
||||||
x-show="!$store.filter.q.trim() || $el.dataset.search.includes($store.filter.q.trim().toLowerCase())">
|
x-show="!$store.filter.q.trim() || $el.dataset.search.includes($store.filter.q.trim().toLowerCase())">
|
||||||
<td>
|
<td>
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if p.driver_id %}
|
{% if p.driver_id %}
|
||||||
<span class="badge ok">{{ ico.i('check', 12) }}<span x-data x-text="$store.i18n.t('ready_to_export')">Ready</span></span>
|
<span class="badge ok">{{ ico.i('check', 12) }}<span x-data x-text="$store.i18n.t('ready_to_export')">Ready</span></span>
|
||||||
<span class="cell-sub">{{ p.driver.original_filename }}</span>
|
<span class="cell-sub">{{ p.driver.label }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge warn">{{ ico.i('alert', 12) }}<span x-data x-text="$store.i18n.t('needs_driver')">Driver needed</span></span>
|
<span class="badge warn">{{ ico.i('alert', 12) }}<span x-data x-text="$store.i18n.t('needs_driver')">Driver needed</span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<option value="" x-text="$store.i18n.t('no_driver_option')">No driver</option>
|
<option value="" x-text="$store.i18n.t('no_driver_option')">No driver</option>
|
||||||
{% for item in driver_data %}
|
{% for item in driver_data %}
|
||||||
<option value="{{ item.driver.id }}">
|
<option value="{{ item.driver.id }}">
|
||||||
{{ item.driver.original_filename }}{% if item.names %} — {{ item.names | join(', ') }}{% endif %}
|
{{ item.driver.label }}{% if item.names %} — {{ item.names | join(', ') }}{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ def test_theme_cycles_on_click(page, live_server: str) -> None:
|
|||||||
"""Clicking theme button cycles data-theme attribute: auto -> light -> dark -> auto."""
|
"""Clicking theme button cycles data-theme attribute: auto -> light -> dark -> auto."""
|
||||||
page.goto(f"{live_server}/", wait_until="domcontentloaded")
|
page.goto(f"{live_server}/", wait_until="domcontentloaded")
|
||||||
|
|
||||||
# Initial state: auto (default from base.html)
|
# Initial state: auto — the attribute must be ABSENT, not "auto". Pico's
|
||||||
|
# OS-dark block is keyed on `:root:not([data-theme])`, so any value pins
|
||||||
|
# Pico to its light theme while app.css follows the OS into dark.
|
||||||
initial_theme = page.evaluate("document.documentElement.getAttribute('data-theme')")
|
initial_theme = page.evaluate("document.documentElement.getAttribute('data-theme')")
|
||||||
assert initial_theme == "auto"
|
assert initial_theme is None
|
||||||
|
|
||||||
# Click once -> light
|
# Click once -> light
|
||||||
page.click("button[aria-label='auto']")
|
page.click("button[aria-label='auto']")
|
||||||
@@ -27,6 +29,13 @@ def test_theme_cycles_on_click(page, live_server: str) -> None:
|
|||||||
)
|
)
|
||||||
assert page.evaluate("document.documentElement.getAttribute('data-theme')") == "dark"
|
assert page.evaluate("document.documentElement.getAttribute('data-theme')") == "dark"
|
||||||
|
|
||||||
|
# Click again -> auto, which removes the attribute again
|
||||||
|
page.click("button[aria-label='dark']")
|
||||||
|
page.wait_for_function(
|
||||||
|
"!document.documentElement.hasAttribute('data-theme')",
|
||||||
|
timeout=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_theme_persists_across_reload(page, live_server: str) -> None:
|
def test_theme_persists_across_reload(page, live_server: str) -> None:
|
||||||
"""After clicking theme toggle, the chosen theme is restored on reload."""
|
"""After clicking theme toggle, the chosen theme is restored on reload."""
|
||||||
|
|||||||
Reference in New Issue
Block a user