Files
ImpTune/imptune/templates/base.html
T
kawa 3353f4546c feat(11-03): Alpine.js theme + i18n stores in base.html with top-right controls
- Add Alpine.store('theme') for Light/Dark/System cycling with localStorage persistence
- Add Alpine.store('i18n') with FR/EN translations for all static UI strings
- Add topbar with theme toggle button (:aria-label, @click cycle) and lang button
- Wrap main content in .main-wrapper + .topbar for layout structure
- Add .main-wrapper, .topbar, .topbar-controls styles to app.css
- Add test_theme_toggle_present integration test
- Fix test_dashboard_shows_recent_printers assertion (string now in i18n JS too)
2026-04-15 11:08:52 +02:00

158 lines
5.8 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ImpTune</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css">
<script>
document.addEventListener('alpine:init', () => {
// Theme store: cycles Light -> Dark -> System, persists in localStorage
Alpine.store('theme', {
current: localStorage.getItem('imptune_theme') || 'auto',
icons: { light: '&#9728;', dark: '&#9790;', auto: '&#9681;' },
init() {
document.documentElement.setAttribute('data-theme', this.current);
},
cycle() {
const order = ['light', 'dark', 'auto'];
this.current = order[(order.indexOf(this.current) + 1) % order.length];
localStorage.setItem('imptune_theme', this.current);
document.documentElement.setAttribute('data-theme', this.current);
}
});
// i18n store: FR/EN toggle, persists in localStorage
Alpine.store('i18n', {
lang: localStorage.getItem('imptune_lang') || 'fr',
t(key) {
return (this.translations[this.lang] || {})[key] || key;
},
toggle() {
this.lang = this.lang === 'fr' ? 'en' : 'fr';
localStorage.setItem('imptune_lang', this.lang);
},
translations: {
fr: {
dashboard: 'Tableau de bord',
drivers: 'Pilotes',
printers: 'Imprimantes',
clients: 'Clients',
packages: 'Paquets',
add_printer: 'Ajouter une imprimante',
add_client: 'Ajouter un client',
printer_library: 'Biblioth\u00e8que d\u2019imprimantes',
edit: 'Modifier',
delete: 'Supprimer',
save: 'Enregistrer',
cancel: 'Annuler',
upload_driver: 'T\u00e9l\u00e9charger un pilote',
printer_name: 'Nom de l\u2019imprimante',
ip_address: 'Adresse IP',
port_name: 'Nom du port',
driver: 'Pilote',
duplex_mode: 'Mode recto-verso',
one_sided: 'Recto simple',
long_edge: 'Grand c\u00f4t\u00e9',
short_edge: 'Petit c\u00f4t\u00e9',
color_mode: 'Mode couleur',
paper_size: 'Format papier',
collate: 'Assembler',
client: 'Client',
edit_printer: 'Modifier l\u2019imprimante',
no_printers: 'Aucune imprimante configur\u00e9e.',
no_clients: 'Aucun client configur\u00e9.',
client_list: 'Liste des clients',
name: 'Nom',
created: 'Cr\u00e9\u00e9 le',
back_to_printers: 'Retour aux imprimantes',
theme_label: 'Th\u00e8me',
lang_label: 'FR'
},
en: {
dashboard: 'Dashboard',
drivers: 'Drivers',
printers: 'Printers',
clients: 'Clients',
packages: 'Packages',
add_printer: 'Add Printer',
add_client: 'Add Client',
printer_library: 'Printer Library',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel',
upload_driver: 'Upload Driver',
printer_name: 'Printer Name',
ip_address: 'IP Address',
port_name: 'Port Name',
driver: 'Driver',
duplex_mode: 'Duplex Mode',
one_sided: 'One-Sided',
long_edge: 'Long Edge',
short_edge: 'Short Edge',
color_mode: 'Color Mode',
paper_size: 'Paper Size',
collate: 'Collate',
client: 'Client',
edit_printer: 'Edit Printer',
no_printers: 'No printers configured yet.',
no_clients: 'No clients configured yet.',
client_list: 'Client List',
name: 'Name',
created: 'Created',
back_to_printers: 'Back to Printers',
theme_label: 'Theme',
lang_label: 'EN'
}
}
});
});
</script>
<script defer src="/static/alpine.min.js"></script>
<script src="/static/htmx.min.js"></script>
</head>
<body>
<div class="layout">
<nav class="sidebar">
<div class="sidebar-brand">
<strong>ImpTune</strong>
</div>
<ul class="sidebar-nav">
<li><a href="/" {% if request.url.path == "/" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('dashboard')">Dashboard</a></li>
<li><a href="/drivers" {% if request.url.path == "/drivers" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('drivers')">Drivers</a></li>
<li><a href="/printers" {% if request.url.path == "/printers" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('printers')">Printers</a></li>
<li><a href="/clients" {% if request.url.path == "/clients" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('clients')">Clients</a></li>
<li><a href="/packages" {% if request.url.path == "/packages" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('packages')">Packages</a></li>
</ul>
</nav>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-controls" x-data>
<!-- Theme toggle button: cycles Light -> Dark -> System -->
<button class="secondary outline"
x-html="$store.theme.icons[$store.theme.current]"
:aria-label="$store.theme.current"
@click="$store.theme.cycle()"
title="Toggle theme">&#9681;</button>
<!-- Language toggle button -->
<button class="secondary outline"
x-text="$store.i18n.t('lang_label')"
@click="$store.i18n.toggle()"
title="Toggle language">FR</button>
</div>
</header>
<main class="main-content">
{% block content %}{% endblock %}
</main>
</div>
</div>
</body>
</html>