All optional entity selectors now use the format "None (description)" with i18n keys instead of hardcoded "—" or bare "None". Added common.none_no_cspt, common.none_no_input, common.none_own_speed keys to all 3 locales. Updated frontend context with the convention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
225 lines
12 KiB
Markdown
225 lines
12 KiB
Markdown
# 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
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="my-field" data-i18n="my.label">Label:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="my.label.hint">Hint text</small>
|
|
<input type="text" id="my-field">
|
|
</div>
|
|
```
|
|
|
|
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 `<select>` with real options only and let the first one be selected by default.
|
|
|
|
### Empty/None option format
|
|
|
|
When a selector has an optional entity (e.g., sync clock, processing template, brightness source), the empty option must use the format `None (<description>)` where the description explains what happens when nothing is selected. Use i18n keys, never hardcoded `—` or bare `None`.
|
|
|
|
Examples:
|
|
- `None (no processing template)` — `t('common.none_no_cspt')`
|
|
- `None (no input source)` — `t('common.none_no_input')`
|
|
- `None (use own speed)` — `t('common.none_own_speed')`
|
|
- `None (full brightness)` — `t('color_strip.composite.brightness.none')`
|
|
- `None (device brightness)` — `t('targets.brightness_vs.none')`
|
|
|
|
For `EntitySelect` with `allowNone: true`, pass the same i18n string as `noneLabel`.
|
|
|
|
### Enhanced selectors (IconSelect & EntitySelect)
|
|
|
|
Plain `<select>` 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 `<select>` with a visual grid of icon+label+description cells. See `_ensureCSSTypeIconSelect()`, `_ensureEffectTypeIconSelect()`, `_ensureInterpolationIconSelect()` in `color-strips.js` for examples.
|
|
|
|
- **Entity references** (picture sources, audio sources, devices, templates, clocks) → use `EntitySelect` from `js/core/entity-palette.js`. This replaces the `<select>` 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 `<select>` but keep it in the DOM with its value in sync. After programmatically changing the `<select>` value, call `.refresh()` (EntitySelect) or `.setValue(val)` (IconSelect) to update the trigger display. Call `.destroy()` when the modal closes.
|
|
|
|
### 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: `<span class="stream-card-prop stream-card-link" onclick="event.stopPropagation(); navigateToCard('streams','audio','audio-multi','data-id','${id}')">🎵 Name</span>`
|
|
|
|
### Modal footer buttons
|
|
|
|
Use **icon-only** buttons (✓ / ✕) matching the device settings modal pattern, **not** text buttons:
|
|
|
|
```html
|
|
<div class="modal-footer">
|
|
<button class="btn btn-icon btn-secondary" onclick="closeMyModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
|
<button class="btn btn-icon btn-primary" onclick="saveMyEntity()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">✓</button>
|
|
</div>
|
|
```
|
|
|
|
### 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
|
|
<label for="my-slider"><span data-i18n="my.label">Speed:</span> <span id="my-slider-display">1.0</span></label>
|
|
...
|
|
<input type="range" id="my-slider" min="0" max="10" step="0.1" value="1.0"
|
|
oninput="document.getElementById('my-slider-display').textContent = this.value">
|
|
```
|
|
|
|
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`).
|
|
|
|
## 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 `<script>` blocks (where `t()` may not be available yet): use `window.t ? t('key') : 'fallback'`
|
|
- In HTML templates: use `data-i18n="key"` for text content, `data-i18n-title="key"` for title attributes, `data-i18n-aria-label="key"` for aria-labels
|
|
- Keys follow dotted namespace convention: `feature.context.description` (e.g. `device.error.brightness`, `calibration.saved`)
|
|
|
|
### Dynamic content and language changes
|
|
|
|
When a feature module generates HTML with baked-in `t()` calls (e.g., toolbar button titles, legend text), that content won't update when the user switches language. To handle this, listen for the `languageChanged` event and re-render:
|
|
|
|
```javascript
|
|
document.addEventListener('languageChanged', () => {
|
|
if (_initialized) _reRender();
|
|
});
|
|
```
|
|
|
|
Static HTML using `data-i18n` attributes is handled automatically by the i18n system. Only dynamically generated HTML needs this pattern.
|
|
|
|
## Bundling & Development Workflow
|
|
|
|
The frontend uses **esbuild** to bundle all JS modules and CSS files into single files for production.
|
|
|
|
### Files
|
|
|
|
- **Entry points:** `static/js/app.js` (JS), `static/css/all.css` (CSS imports all individual sheets)
|
|
- **Output:** `static/dist/app.bundle.js` and `static/dist/app.bundle.css` (minified + source maps)
|
|
- **Config:** `server/esbuild.mjs`
|
|
- **HTML:** `templates/index.html` references the bundles, not individual source files
|
|
|
|
### Commands (from `server/` directory)
|
|
|
|
| Command | Description |
|
|
|---|---|
|
|
| `npm run build` | One-shot bundle + minify (~30ms) |
|
|
| `npm run watch` | Watch mode — auto-rebuilds on any JS/CSS file save |
|
|
|
|
### Development workflow
|
|
|
|
1. Run `npm run watch` in a terminal (stays running)
|
|
2. Edit source files in `static/js/` or `static/css/` as usual
|
|
3. esbuild rebuilds the bundle automatically (~30ms)
|
|
4. Refresh the browser to see changes
|
|
|
|
### Dependencies
|
|
|
|
All JS/CSS dependencies are bundled — **no CDN or external requests** at runtime:
|
|
|
|
- **Chart.js** — imported in `perf-charts.js`, exposed as `window.Chart` for `targets.js` and `dashboard.js`
|
|
- **ELK.js** — imported in `graph-layout.js` for graph auto-layout
|
|
- **Fonts** — DM Sans (400-700) and Orbitron (700) woff2 files in `static/fonts/`, declared in `css/fonts.css`
|
|
|
|
When adding a new JS dependency: `npm install <pkg>` in `server/`, then `import` it in the relevant source file. esbuild bundles it automatically.
|
|
|
|
### Notes
|
|
|
|
- The `dist/` directory is gitignored — bundles are build artifacts, run `npm run build` after clone
|
|
- Source maps are generated so browser DevTools show original source files
|
|
- The server sets `Cache-Control: no-cache` on static JS/CSS/JSON to prevent stale browser caches during development
|
|
- GZip compression middleware reduces transfer sizes by ~75%
|
|
- **Do not edit files in `static/dist/`** — they are overwritten by the build
|
|
|
|
## Chrome Browser Tools
|
|
|
|
See [`contexts/chrome-tools.md`](chrome-tools.md) for Chrome MCP tool usage, browser tricks (hard reload, zoom, console), and verification workflow.
|
|
|
|
## Visual Graph Editor
|
|
|
|
See [`contexts/graph-editor.md`](graph-editor.md) for full graph editor architecture and conventions.
|
|
|
|
**IMPORTANT:** When adding or modifying entity types, subtypes, or connection fields, the graph editor files **must** be updated in sync. The graph maintains its own maps of entity colors, labels, icons, connection rules, and cache references. See the "Keeping the graph in sync with entity types" section in `graph-editor.md` for the complete checklist.
|