diff --git a/CLAUDE.md b/CLAUDE.md index 1d54bf9..ee96ca9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,6 +177,48 @@ socket |> assign(form: to_form(changeset)) - Avoid LiveComponents unless you have a specific need (isolated state, targeted updates) - Never use deprecated `phx-update="append"` or `phx-update="prepend"` +## Progressive Enhancement + +Build HTML+CSS that works without JS. LiveView layers on top. + +**Principle:** If a state change is purely visual (which radio is selected, which panel is shown), handle it in CSS. Don't round-trip to the server for something the browser can do alone. + +**Pattern for forms with dynamic sections:** + +1. Render ALL possible states in the HTML (e.g. all adapter configs, all option panels) +2. Use CSS `:has(:checked)` to show the matching section and hide the rest +3. Namespace field names to avoid clashes (`email[brevo][api_key]`, not `email[api_key]`) +4. Give the form both `action` (no-JS POST) and `phx-submit` (LiveView) +5. Add a thin controller to handle the no-JS POST path + +```css +/* Hide all sections by default, show the one matching the checked radio */ +.config-section[data-option] { display: none; } +form:has(#option-foo:checked) [data-option="foo"] { display: block; } +``` + +**JS-only elements:** Wrap in a container with an ID, hide it via ` +``` + +**HEEx and `") +end +``` + ## JS/CSS Guidelines - Project is fully Tailwind-free — hand-written CSS with `@layer`, native nesting, `oklch()`