Files
wled-screen-controller-mixed/contexts/frontend.md
alexei.dolgolyov 49c2a63d68 Bundle frontend with esbuild, serve fonts offline, fix dashboard
- Add esbuild bundling: JS (IIFE, minified, sourcemapped) and CSS into
  single dist/ files, replacing 15+ individual CSS links and CDN scripts
- Bundle Chart.js and ELK.js from npm instead of CDN (fully offline)
- Serve DM Sans and Orbitron fonts locally from static/fonts/
- Fix dashboard automation card stretching full width (max-width: 500px)
- Fix time_of_day condition not localized in automation cards
- Add Chrome browser tools context file for MCP testing workflow
- Update frontend context with bundling docs and Chrome tools reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:20:20 +03:00

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

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:

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 for Chrome MCP tool usage, browser tricks (hard reload, zoom, console), and verification workflow.

Visual Graph Editor

See contexts/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.