Skip to content

feat(ui): redesign web UI in the "Gallery" visual language#25

Open
bring42 wants to merge 1 commit into
mainfrom
claude/dazzling-bhaskara-494b8d
Open

feat(ui): redesign web UI in the "Gallery" visual language#25
bring42 wants to merge 1 commit into
mainfrom
claude/dazzling-bhaskara-494b8d

Conversation

@bring42

@bring42 bring42 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Full restyle of data/index.html + app.css + app.js to the Gallery direction: porcelain-light, monochrome-precise chrome where colour appears only where it represents actual light (a live strip readout + effect swatches, both approximated client-side). Direction was picked by the owner from two interactive mockups pitched against docs/premium-lighting-criteria.md.
  • Motion is eased throughout — power-off fades the strip to standby rather than snapping, no emoji, hairline structure over boxed cards, tabular numerals, system fonts (kept intentionally: offline, LittleFS budget).
  • Effect rail now builds dynamically from GET /api/v2/effects (was 23 hardcoded emoji tiles). Removed dead legacy palette/speed/intensity/color controls, the light/dark theme toggle, and the 404'ing scenes UI.
  • Net smaller: app.js 63KB → 32KB; total UI 62KB uncompressed / 16KB gzipped.
  • Adds scripts/mock_server.py + .claude/launch.json: a dev-only mock of the device HTTP API (serves the real data/ files, fakes v2 endpoints with 202 semantics) so the UI can be iterated and screenshotted in a browser without hardware — mklittlefs is still x86-broken locally.

Note: this lands the visual system — page structure is still the original's card stack. A follow-up will rework the layout itself (see conversation).

Firmware and API are unchanged.

Test plan

  • Verified in-browser against scripts/mock_server.py: effect switch → schema params render (int/float/color/bool/enum/palette) → apply serializes correctly and reconciles (202 contract)
  • Power-off fade + Standby readout, brightness slider, segment selector + add/split/delete/rename, nightlight expand, settings load/save
  • No dangling onclick/$() references after the rewrite (swept for orphans)
  • Verify on real hardware once flashable (not done this session)

🤖 Generated with Claude Code

Full restyle of data/index.html + app.css + app.js: porcelain-light,
monochrome-precise chrome where colour appears only where it represents
actual light (a new live strip readout + effect swatches, both approximating
the active effect client-side). Motion is eased throughout (power-off fades
to standby rather than snapping), no emoji, hairline structure over boxed
cards, tabular numerals, system fonts (offline / LittleFS budget).

Direction was picked by the owner from two interactive mockups pitched
against docs/premium-lighting-criteria.md, replacing the prior cobalt-on-black
admin-panel look. This lands the visual system; a follow-up will rework the
page structure itself (the current layout is still the original's card stack).

The effect rail now builds dynamically from GET /api/v2/effects (was 23
hardcoded emoji tiles); dead legacy palette/speed/intensity/color controls,
the light/dark theme toggle, and the 404'ing scenes UI are removed. Net
smaller: app.js 63KB -> 32KB, total UI 62KB uncompressed / 16KB gzipped.

Also adds scripts/mock_server.py + .claude/launch.json: a dev-only mock of
the device HTTP API (serves the real data/ files, fakes v2 endpoints with
202 semantics) so the UI can be iterated and screenshotted in a browser
without hardware, since mklittlefs is still x86-broken on this Mac.

Firmware and API are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 18:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the “Gallery” redesign of the LittleFS-served web UI (HTML/CSS/JS), moving effect selection + parameter controls to a schema-driven v2 API model, and adds a dev-only HTTP mock server to iterate on the UI without hardware.

Changes:

  • Restyles data/index.html + app.css into the new “Gallery” visual language and updates markup to match the new interaction model.
  • Rewrites app.js to build the effect rail dynamically from GET /api/v2/effects and render controls from each effect’s schema.
  • Adds a Python mock server (plus a .claude launcher config) for local UI development without an ESP32 device.

Reviewed changes

Copilot reviewed 5 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/mock_server.py Dev-only HTTP mock for serving data/ and faking key v2 + legacy endpoints for UI iteration.
data/index.html Updates page structure to the new Gallery layout and hooks into the rewritten JS behaviors.
data/assets/app.js Schema-driven UI logic (effect rail, param controls, power/brightness, settings modal, nightlight, live strip approximation).
data/assets/app.css Implements the Gallery design system styles and interaction affordances (motion, rails, modal, controls).
.claude/launch.json Convenience launcher configuration for running the mock server in development.

Comment thread data/assets/app.js
Comment on lines +280 to +304
async function applySegmentState() {
const effectId = $('effect').value;
if (!effectId) return;
const segment = { effect: effectId };
const meta = effectMetadata[effectId];
if (meta && meta.hasSchema) {
const params = {};
let palette = null;
document.querySelectorAll('#schemaControls > .ctl').forEach(group => {
const pid = group.dataset.paramId;
let input = group.querySelector('input') || group.querySelector('select');
if (!input) return;
let value;
if (input.type === 'checkbox') value = input.checked;
else if (input.type === 'number') value = parseFloat(input.value);
else if (input.type === 'range') value = parseInt(input.value);
else if (input.type === 'color') value = input.value;
else if (input.tagName === 'SELECT') value = parseInt(input.value);
else value = input.value;
if (pid === 'palette') palette = PALETTE_PRESETS[input.value] ?? 0;
else params[pid] = value;
});
if (Object.keys(params).length) segment.params = params;
if (palette !== null) segment.palette = palette;
}
Comment thread data/assets/app.js
Comment on lines +251 to +255
tile.onclick = () => {
hidden.value = name;
row.querySelectorAll('.pal-tile').forEach(t => t.classList.toggle('sel', t === tile));
applySegmentState();
};
Comment thread scripts/mock_server.py
Comment on lines +98 to +105
def _static(self, path):
rel = "index.html" if path == "/" else path.lstrip("/")
fp = os.path.normpath(os.path.join(ROOT, rel))
if not fp.startswith(os.path.normpath(ROOT)) or not os.path.isfile(fp):
return self._send(404, {"error": "not_found"})
ct = {"html": "text/html", "css": "text/css", "js": "application/javascript"}.get(fp.rsplit(".", 1)[-1], "text/plain")
with open(fp, "rb") as f:
self._send(200, f.read(), ct)
Comment thread data/assets/app.js
Comment on lines +247 to +250
const tile = document.createElement('div');
tile.className = 'pal-tile' + (name === hidden.value ? ' sel' : '');
tile.dataset.value = name;
tile.innerHTML = `<div class="pal-swatch" style="background:${paletteGradientCss(name)}"></div><div class="pal-nm">${name[0].toUpperCase()+name.slice(1)}</div>`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants