Add a dark theme to the admin portal. By default the theme follows the operating system setting; a switch in the top right corner of the admin navbar lets the user force light or dark.
Current state
The UI is the Sneat free template in assets/vendor/ (Bootstrap 5 before 5.3, so there is no data-bs-theme support). It ships a light theme only.
core.css contains about 960 hard-coded colour literals and theme-default.css about 180. Only a few values go through CSS variables (--bs-body-bg, --bs-body-color). There are just two .dark-style rules in core.css (form validation), so the template has a naming convention for dark mode but no implementation.
src/components/header.rs already sets class="light-style layout-menu-fixed ..." on <html> — the hook the template expects (light-style / dark-style).
Header is rendered outside <Router> and applies to every page. The public booking page and the admin portal are one single-page app, and the admin "Create booking" dialog embeds the public form (<Public/>).
Charts on /admin/bookings are rendered on the server by charts-rs with THEME_ANT (light background) and sent as an image (chart() in src/backend/reservation.rs). charts-rs also has THEME_DARK.
src/pages/all_reservations.rs uses navbar-light bg-light for the year/month bar.
leptos-use (already a dependency, with the ssr and actix features enabled) provides use_color_mode / use_preferred_dark and cookie storage.
style/main.scss is compiled to /pkg/rezervator.css, but that file is not linked anywhere in header.rs, so it has no effect. Custom CSS that is actually loaded lives in assets/ (assets/css/demo.css, assets/vendor/css/control.css, assets/data/banner.css).
Proposed solution
Dark stylesheet — a new app-owned file (e.g. assets/css/dark.css), linked in header.rs after theme-default.css. It overrides only the components the admin portal uses: body, cards, navbar, side menu, footer, tables, forms, modals, dropdowns, alerts, outline buttons, links, borders. Vendor files stay untouched.
Three modes: auto / light / dark.
auto (default) follows prefers-color-scheme in CSS, so it works on the first visit without JavaScript and without a flash.
light / dark are set with a class on <html> (light-style / dark-style).
Write the dark colours once as CSS custom properties and apply them both under html.dark-style and under @media (prefers-color-scheme: dark) for the auto mode, to avoid duplicating every rule.
Persistence in a cookie, not localStorage: the server must render the same <html> class as the client, otherwise hydration differs and the page flashes. use_color_mode with cookie storage is a candidate.
Switch in the navbar of src/components/admin_portal.rs, next to the settings and user icons (Boxicons bx-sun / bx-moon), cycling auto → light → dark. Labels go through trl() with Czech translations.
Charts — pass the active theme to month_chart / year_chart and use THEME_DARK in dark mode. In auto mode the server does not know the system preference, so the client has to send the resolved value.
Replace navbar-light bg-light in all_reservations.rs with classes that follow the theme.
Open questions
Scope: admin portal only (/admin*), or also the login page and the public booking page? The public page has a customer-defined banner and text, so it is proposed to keep it light for now. Because Header is global, limiting the theme to /admin* needs the class to depend on the current route.
Should the embedded public form inside the admin "Create booking" dialog be dark? It will be, unless it is explicitly excluded.
Simple light/dark toggle, or the three-state auto/light/dark switch proposed above?
Out of scope
Dark theme for e-mails.
Editing or replacing the vendor template.
Done when
With no stored preference the admin portal follows the system setting, including on the first server-rendered load (no flash of the wrong theme).
The switch changes the theme immediately and the choice survives a reload and a new session.
All admin pages and dialogs are readable in both themes: overview, settings, mail settings, bookings (including charts), customers, appearance.
Full reload of every admin page shows no hydration error in the browser console in either theme.
Both cargo check commands and cargo test --features ssr pass.
Add a dark theme to the admin portal. By default the theme follows the operating system setting; a switch in the top right corner of the admin navbar lets the user force light or dark.
## Current state
- The UI is the Sneat free template in `assets/vendor/` (Bootstrap 5 before 5.3, so there is no `data-bs-theme` support). It ships a light theme only.
- `core.css` contains about 960 hard-coded colour literals and `theme-default.css` about 180. Only a few values go through CSS variables (`--bs-body-bg`, `--bs-body-color`). There are just two `.dark-style` rules in `core.css` (form validation), so the template has a naming convention for dark mode but no implementation.
- `src/components/header.rs` already sets `class="light-style layout-menu-fixed ..."` on `<html>` — the hook the template expects (`light-style` / `dark-style`).
- `Header` is rendered outside `<Router>` and applies to every page. The public booking page and the admin portal are one single-page app, and the admin "Create booking" dialog embeds the public form (`<Public/>`).
- Charts on `/admin/bookings` are rendered on the server by `charts-rs` with `THEME_ANT` (light background) and sent as an image (`chart()` in `src/backend/reservation.rs`). `charts-rs` also has `THEME_DARK`.
- `src/pages/all_reservations.rs` uses `navbar-light bg-light` for the year/month bar.
- `leptos-use` (already a dependency, with the `ssr` and `actix` features enabled) provides `use_color_mode` / `use_preferred_dark` and cookie storage.
- `style/main.scss` is compiled to `/pkg/rezervator.css`, but that file is not linked anywhere in `header.rs`, so it has no effect. Custom CSS that is actually loaded lives in `assets/` (`assets/css/demo.css`, `assets/vendor/css/control.css`, `assets/data/banner.css`).
## Proposed solution
1. **Dark stylesheet** — a new app-owned file (e.g. `assets/css/dark.css`), linked in `header.rs` after `theme-default.css`. It overrides only the components the admin portal uses: body, cards, navbar, side menu, footer, tables, forms, modals, dropdowns, alerts, outline buttons, links, borders. Vendor files stay untouched.
2. **Three modes: auto / light / dark.**
- `auto` (default) follows `prefers-color-scheme` in CSS, so it works on the first visit without JavaScript and without a flash.
- `light` / `dark` are set with a class on `<html>` (`light-style` / `dark-style`).
- Write the dark colours once as CSS custom properties and apply them both under `html.dark-style` and under `@media (prefers-color-scheme: dark)` for the auto mode, to avoid duplicating every rule.
3. **Persistence in a cookie**, not `localStorage`: the server must render the same `<html>` class as the client, otherwise hydration differs and the page flashes. `use_color_mode` with cookie storage is a candidate.
4. **Switch** in the navbar of `src/components/admin_portal.rs`, next to the settings and user icons (Boxicons `bx-sun` / `bx-moon`), cycling auto → light → dark. Labels go through `trl()` with Czech translations.
5. **Charts** — pass the active theme to `month_chart` / `year_chart` and use `THEME_DARK` in dark mode. In auto mode the server does not know the system preference, so the client has to send the resolved value.
6. Replace `navbar-light bg-light` in `all_reservations.rs` with classes that follow the theme.
## Open questions
- **Scope**: admin portal only (`/admin*`), or also the login page and the public booking page? The public page has a customer-defined banner and text, so it is proposed to keep it light for now. Because `Header` is global, limiting the theme to `/admin*` needs the class to depend on the current route.
- Should the embedded public form inside the admin "Create booking" dialog be dark? It will be, unless it is explicitly excluded.
- Simple light/dark toggle, or the three-state auto/light/dark switch proposed above?
## Out of scope
- Dark theme for e-mails.
- Editing or replacing the vendor template.
## Done when
- With no stored preference the admin portal follows the system setting, including on the first server-rendered load (no flash of the wrong theme).
- The switch changes the theme immediately and the choice survives a reload and a new session.
- All admin pages and dialogs are readable in both themes: overview, settings, mail settings, bookings (including charts), customers, appearance.
- Full reload of every admin page shows no hydration error in the browser console in either theme.
- Both `cargo check` commands and `cargo test --features ssr` pass.
Scope: admin portal only. The login page and the public booking page stay as they are.
The public form embedded in the admin "Create booking" dialog may be dark.
Three-state switch: auto / light / dark.
Decisions on the open questions:
1. **Scope**: admin portal only. The login page and the public booking page stay as they are.
2. The public form embedded in the admin "Create booking" dialog may be dark.
3. Three-state switch: auto / light / dark.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Add a dark theme to the admin portal. By default the theme follows the operating system setting; a switch in the top right corner of the admin navbar lets the user force light or dark.
Current state
assets/vendor/(Bootstrap 5 before 5.3, so there is nodata-bs-themesupport). It ships a light theme only.core.csscontains about 960 hard-coded colour literals andtheme-default.cssabout 180. Only a few values go through CSS variables (--bs-body-bg,--bs-body-color). There are just two.dark-stylerules incore.css(form validation), so the template has a naming convention for dark mode but no implementation.src/components/header.rsalready setsclass="light-style layout-menu-fixed ..."on<html>— the hook the template expects (light-style/dark-style).Headeris rendered outside<Router>and applies to every page. The public booking page and the admin portal are one single-page app, and the admin "Create booking" dialog embeds the public form (<Public/>)./admin/bookingsare rendered on the server bycharts-rswithTHEME_ANT(light background) and sent as an image (chart()insrc/backend/reservation.rs).charts-rsalso hasTHEME_DARK.src/pages/all_reservations.rsusesnavbar-light bg-lightfor the year/month bar.leptos-use(already a dependency, with thessrandactixfeatures enabled) providesuse_color_mode/use_preferred_darkand cookie storage.style/main.scssis compiled to/pkg/rezervator.css, but that file is not linked anywhere inheader.rs, so it has no effect. Custom CSS that is actually loaded lives inassets/(assets/css/demo.css,assets/vendor/css/control.css,assets/data/banner.css).Proposed solution
assets/css/dark.css), linked inheader.rsaftertheme-default.css. It overrides only the components the admin portal uses: body, cards, navbar, side menu, footer, tables, forms, modals, dropdowns, alerts, outline buttons, links, borders. Vendor files stay untouched.auto(default) followsprefers-color-schemein CSS, so it works on the first visit without JavaScript and without a flash.light/darkare set with a class on<html>(light-style/dark-style).html.dark-styleand under@media (prefers-color-scheme: dark)for the auto mode, to avoid duplicating every rule.localStorage: the server must render the same<html>class as the client, otherwise hydration differs and the page flashes.use_color_modewith cookie storage is a candidate.src/components/admin_portal.rs, next to the settings and user icons (Boxiconsbx-sun/bx-moon), cycling auto → light → dark. Labels go throughtrl()with Czech translations.month_chart/year_chartand useTHEME_DARKin dark mode. In auto mode the server does not know the system preference, so the client has to send the resolved value.navbar-light bg-lightinall_reservations.rswith classes that follow the theme.Open questions
/admin*), or also the login page and the public booking page? The public page has a customer-defined banner and text, so it is proposed to keep it light for now. BecauseHeaderis global, limiting the theme to/admin*needs the class to depend on the current route.Out of scope
Done when
cargo checkcommands andcargo test --features ssrpass.Decisions on the open questions: