Files
wled-screen-controller-mixed/contexts/frontend.md
alexei.dolgolyov e912019873 Improve CSS test preview: HD resolution, screen-only border, and refactor frontend docs
- 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>
2026-03-13 01:50:23 +03:00

8.1 KiB

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:

<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:

    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>

Use icon-only buttons (✓ / ✕) matching the device settings modal pattern, not text buttons:

<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">&#x2715;</button>
    <button class="btn btn-icon btn-primary" onclick="saveMyEntity()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">&#x2713;</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:

<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)