# Frontend Rules & Conventions **Read this file when working on frontend tasks** (HTML, CSS, JS, locales, templates). ## CSS Custom Properties (Variables) Defined in `server/src/wled_controller/static/css/base.css`. **IMPORTANT:** There is NO `--accent` variable. Always use `--primary-color` for accent/brand color. ### Global (`:root`) | Variable | Value | Usage | |---|---|---| | `--primary-color` | `#4CAF50` | **Accent/brand color** — borders, highlights, active states | | `--primary-hover` | `#5cb860` | Hover state for primary elements | | `--primary-contrast` | `#ffffff` | Text on primary background | | `--danger-color` | `#f44336` | Destructive actions, errors | | `--warning-color` | `#ff9800` | Warnings | | `--info-color` | `#2196F3` | Informational highlights | ### Theme-specific (`[data-theme="dark"]` / `[data-theme="light"]`) | Variable | Dark | Light | Usage | |---|---|---|---| | `--bg-color` | `#1a1a1a` | `#f5f5f5` | Page background | | `--bg-secondary` | `#242424` | `#eee` | Secondary background | | `--card-bg` | `#2d2d2d` | `#ffffff` | Card/panel background | | `--text-color` | `#e0e0e0` | `#333333` | Primary text | | `--text-secondary` | `#999` | `#666` | Secondary text | | `--text-muted` | `#777` | `#999` | Muted/disabled text | | `--border-color` | `#404040` | `#e0e0e0` | Borders, dividers | | `--primary-text-color` | `#66bb6a` | `#3d8b40` | Primary-colored text | | `--success-color` | `#28a745` | `#2e7d32` | Success indicators | | `--shadow-color` | `rgba(0,0,0,0.3)` | `rgba(0,0,0,0.12)` | Box shadows | ## UI Conventions for Dialogs ### Hints Every form field in a modal should have a hint. Use the `.label-row` wrapper with a `?` toggle button: ```html
``` Add hint text to both `en.json` and `ru.json` locale files using a `.hint` suffix on the label key. ### Select dropdowns Do **not** add placeholder options like `-- Select something --`. Populate the `` dropdowns should be enhanced with visual selectors depending on the data type: - **Predefined options** (source types, effect types, palettes, waveforms, viz modes) → use `IconSelect` from `js/core/icon-select.js`. This replaces the `` with a searchable command-palette-style picker. See `_cssPictureSourceEntitySelect` in `color-strips.js` or `_lineSourceEntitySelect` in `advanced-calibration.js` for examples. Both widgets hide the native `` value, call `.refresh()` (EntitySelect) or `.setValue(val)` (IconSelect) to update the trigger display. Call `.destroy()` when the modal closes. **IMPORTANT:** For `IconSelect` item icons, use SVG icons from `js/core/icon-paths.js` (via `_icon(P.iconName)`) or styled `` elements (e.g., `A`). **Never use emoji** — they render inconsistently across platforms and themes. ### Modal dirty check (discard unsaved changes) Every editor modal **must** have a dirty check so closing with unsaved changes shows a "Discard unsaved changes?" confirmation. Use the `Modal` base class pattern from `js/core/modal.js`: 1. **Subclass Modal** with `snapshotValues()` returning an object of all tracked field values: ```javascript class MyEditorModal extends Modal { constructor() { super('my-modal-id'); } snapshotValues() { return { name: document.getElementById('my-name').value, // ... all form fields }; } onForceClose() { // Optional: cleanup (reset flags, clear state, etc.) } } const myModal = new MyEditorModal(); ``` 2. **Call `modal.snapshot()`** after the form is fully populated (after `modal.open()`). 3. **Close/cancel button** calls `await modal.close()` — triggers dirty check + confirmation. 4. **Save function** calls `modal.forceClose()` after successful save — skips dirty check. 5. For complex/dynamic state (filter lists, schedule rows, conditions), serialize to JSON string in `snapshotValues()`. The base class handles: `isDirty()` comparison, confirmation dialog, backdrop click, ESC key, focus trapping, and body scroll lock. ### Card appearance When creating or modifying entity cards (devices, targets, CSS sources, streams, audio/value sources, templates), **always reference existing cards** of the same or similar type for visual consistency. Cards should have: - Clone (📋) and Edit (✏️) icon buttons in `.template-card-actions` - Delete (✕) button as `.card-remove-btn` - Property badges in `.stream-card-props` with emoji icons - **Crosslinks**: When a card references another entity (audio source, picture source, capture template, PP template, etc.), make the property badge a clickable link using the `stream-card-link` CSS class and an `onclick` handler calling `navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue)`. Only add the link when the referenced entity is found (to avoid broken navigation). Example: `🎵 Name` ### Modal footer buttons Use **icon-only** buttons (✓ / ✕) matching the device settings modal pattern, **not** text buttons: ```html ``` ### Slider value display For range sliders, display the current value **inside the label** (not in a separate wrapper). This keeps the value visible next to the property name: ```html ... ``` Do **not** use a `range-with-value` wrapper div. ### Tutorials The app has an interactive tutorial system (`static/js/features/tutorials.js`) with a generic engine, spotlight overlay, tooltip positioning, and keyboard navigation. Tutorials exist for: - **Getting started** (header-level walkthrough of all tabs and controls) - **Per-tab tutorials** (Dashboard, Targets, Sources, Profiles) triggered by `?` buttons - **Device card tutorial** and **Calibration tutorial** (context-specific) When adding **new tabs, sections, or major UI elements**, update the corresponding tutorial step array in `tutorials.js` and add `tour.*` i18n keys to all 3 locale files (`en.json`, `ru.json`, `zh.json`). ## Icons **Always use SVG icons from the icon system, never text/emoji/Unicode symbols for buttons and UI controls.** - Icon SVG paths are defined in `static/js/core/icon-paths.js` (Lucide icons, 24×24 viewBox) - Icon constants are exported from `static/js/core/icons.js` (e.g. `ICON_START`, `ICON_TRASH`, `ICON_EDIT`) - Use `_svg(path)` wrapper from `icons.js` to create new icon constants from paths When you need a new icon: 1. Find the Lucide icon at https://lucide.dev 2. Copy the inner SVG elements (paths, circles, rects) into `icon-paths.js` as a new export 3. Add a corresponding `ICON_*` constant in `icons.js` using `_svg(P.myIcon)` 4. Import and use the constant in your feature module Common icons: `ICON_START` (play), `ICON_STOP` (power), `ICON_EDIT` (pencil), `ICON_CLONE` (copy), `ICON_TRASH` (trash), `ICON_SETTINGS` (gear), `ICON_TEST` (flask), `ICON_OK` (circle-check), `ICON_WARNING` (triangle-alert), `ICON_HELP` (circle-help), `ICON_LIST_CHECKS` (list-checks), `ICON_CIRCLE_OFF` (circle-off). For icon-only buttons, use `btn btn-icon` CSS classes. The `.icon` class inside buttons auto-sizes to 16×16. ## Localization (i18n) **Every user-facing string must be localized.** Never use hardcoded English strings in `showToast()`, `error.textContent`, modal messages, or any other UI-visible text. Always use `t('key')` from `../core/i18n.js` and add the corresponding key to **all three** locale files (`en.json`, `ru.json`, `zh.json`). - In JS modules: `import { t } from '../core/i18n.js';` then `showToast(t('my.key'), 'error')` - In inline `