Files
2026-10-12 22:27:29 +02:00

54 lines
6.4 KiB
Markdown

---
paths:
- "src/pages/**"
- "src/components/**"
- "src/locales/**"
- "src/app.rs"
- "src/validator.rs"
- "style/**"
- "assets/css/**"
---
# Frontend rules
## Components
- Leptos 0.8 API only (`use leptos::prelude::*`). Components are `#[component]` functions returning `impl IntoView`, one page or dialog per file in `src/pages/`; reusable building blocks go to `src/components/`. Register new files in the respective `mod.rs` (`pub mod` only if used from `app.rs`).
- This code runs on the server (SSR) and in the browser (hydration). Browser-only APIs (`window()`, `web_sys`) must not run during SSR — keep them in event handlers/effects or behind `#[cfg(not(feature = "ssr"))]`. Markup must render the same on both sides or hydration breaks.
- A new admin page needs a `<Route path="admin/..."/>` in `src/app.rs` wrapped in `<AdminPortal>`, and a menu entry in `src/components/admin_portal.rs`. Paths starting with `/admin` are login-protected by the middleware; data access is still guarded by `perm_check!` in the server fn.
## Leptos 0.8 pitfalls
- Views are statically typed: `if`/`else` and `match` arms that return different markup must each end with `.into_any()`.
- Context is not available in event handlers (`on:click` etc.). Call `use_context` in the component body and keep the result — `DialogOpener` and `MenuOpener` do this in `new()`, so create them in a component body, not inside a handler.
- Markup must be valid HTML, or hydration panics in the browser and the whole page stops reacting: no block elements (`<div>`, `<table>`, another `<p>`) inside `<p>`, and `<tr>` only inside `<tbody>`/`<thead>`. The compiler doesn't catch this — load the page with a full reload and check the browser console.
- HTML attributes are typed. Deprecated ones (`align`, `width` on `<table>`) don't compile; use `style`.
- A value used twice in one `view!` (e.g. in an attribute and as a child) must be cloned for the first use.
- Client-side form validation hooks into `<ActionForm>` with `on:submit:capture` and `ev.prevent_default()`; a plain `on:submit` runs too late. `DataForm` already does this.
- `action.value()` is not an `RwSignal`; pass it on as `Signal::derive(move || action.value().get())`.
## Data loading and forms
- Load data with `Resource::new_blocking` / `Resource::new` calling the server fn, rendered inside `<Transition fallback=...>` with `{trl("Loading...")}`. Read the resource in a closure — `{move || res.get().map(...)}` — a plain `{res.get()...}` block is evaluated once and never updates. Use a dialog's `opener.visible()` as the resource source so lists reload after the dialog closes (see `src/pages/users.rs`).
- Handle all three outcomes of a server call: `Err(e)`, `Ok(ApiResponse::Error(msg))`, `Ok(ApiResponse::Data(d))`.
- Edit/create dialogs: `DialogOpener::new()` in the parent, `ServerAction::<ServerFnStruct>::new()` in the dialog, wrapped in `<DataForm opener=.. action=.. title="..">`. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use `<QuestionDialog>`.
- Form field names follow the server fn argument: `name="company[name]"`. Pass the id as a hidden input. Fill inputs with `prop:value={move || signal.get().field}`.
- `DataForm`/`ModalDialog` take `title` as an untranslated English key (`&'static str`); they translate it themselves.
## Translations
- Every user-visible string goes through `trl("English text")`; never hard-code Czech (or any text) in a view. The English text is the key and the fallback.
- When you add or change a key, add the Czech translation to the `cs` map in `src/locales/catalogues.rs`. This includes validation `message`s and `ApiResponse::Error` strings from the backend.
- `trl()` returns a closure — use it directly as `{trl("...")}` or attribute value; call it (`trl("...")()`) when a `String` is needed.
- Dates shown to users go through `loc_date()`, weekday names through `show_day()`.
## Styling
- UI is the Bootstrap-based admin template in `assets/vendor/` plus Boxicons (`<i class="bx bx-...">`). Reuse its classes (`card`, `table`, `btn btn-primary`, `form-control`, `row`/`col`) before writing CSS.
- Custom styles go to a CSS file in `assets/css/` and must be linked with `<Link rel="stylesheet">` in `src/components/header.rs` (order matters: after the vendor files to override them). `style/main.scss` is not linked anywhere and has no effect. Never edit `assets/vendor/` (the one app-owned file there is `assets/vendor/css/control.css`).
- The admin portal has a light and a dark theme (`src/components/theme.rs`, switch in the navbar: auto / light / dark, stored in the `theme` cookie). Dark colours are overrides in `assets/css/dark.css`, which `AdminPortal` links with a `media` attribute — the public page and the login page never load it. When you add or restyle something in the admin portal, check it in both themes and add the dark override there; don't hard-code light colours (`bg-light`, `bg-white`, inline `color`) in admin views.
- Charts are drawn on the server, so the chart server fns take a `dark` flag from `Theme::is_dark()`.
- Styles of the public page are in `assets/css/public.css`, scoped by its own classes (`.public-page` wraps the public route only, `.public-banner*`), so they don't leak into the admin portal. The customer's appearance settings reach it as CSS variables in a `style` attribute (`Colors::style()` in `src/backend/color.rs`, `BannerOptions::style()` in `src/backend/banner.rs`, `Appearance::banner_style()` in `src/backend/data.rs`) — and as classes on the wrapper for the layout options (`LayoutOptions::classes()` in `src/backend/layout.rs`); never generate CSS files. A new look option goes the same way: a column with a default equal to the current look, a validated field, a variable or class, a rule in `public.css`, a control in the appearance settings — the markup of the public form stays the same for all values. Every value put into a `style` attribute must be validated first.
- The preview in the appearance settings is a `.public-page.public-preview` element inside the admin portal, styled by the same `public.css`. A rule there must set every property that `dark.css` sets for the same component, otherwise the preview changes with the admin theme. When the public form gets a new element, add it to the preview (`Preview` in `src/pages/appearance_settings.rs`) and check it with a dark palette.
- Stylesheets and meta tags are declared in `src/components/header.rs`.