- Bump capture preview resolution from 480×360 to 960×540 (HD) - Increase preview FPS from 2 to ~12 FPS (AUX_INTERVAL 0.5→0.08) - Add accent-color border on screen rect only (not LED edges) via ::after - Use dynamic aspect-ratio from decoded JPEG frames instead of fixed height - Widen modal to 900px for picture sources - Move frontend conventions from CLAUDE.md to contexts/frontend.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
8.1 KiB
Markdown
147 lines
8.1 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.
|
|
|
|
### 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`)
|