From 0e9c8affd4f5693c9c17e4863397a8a29ede2ed1 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 9 Jun 2026 08:54:51 +1000 Subject: [PATCH] feat(devices): icon picker (Type sets + Brand search) Co-Authored-By: Claude Opus 4.8 --- public/views/icon_picker.js | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 public/views/icon_picker.js diff --git a/public/views/icon_picker.js b/public/views/icon_picker.js new file mode 100644 index 0000000..20fbaae --- /dev/null +++ b/public/views/icon_picker.js @@ -0,0 +1,57 @@ +// public/views/icon_picker.js — inline picker with Type + Brand tabs. +import { el, mount, clear } from '../dom.js'; +import { api } from '../api.js'; +import { resolveIcon } from './icon_util.js'; + +// onPick(ref) called with 'set::' or 'brand:'. Returns an element. +export function iconPicker(currentRef, onPick) { + const box = el('div', { class: 'icon-picker' }); + const tabs = el('div', { class: 'ip-tabs' }); + const body = el('div', { class: 'ip-body' }); + const typeTab = el('button', { class: 'ip-tab active' }, 'Type'); + const brandTab = el('button', { class: 'ip-tab' }, 'Brand'); + typeTab.onclick = () => { typeTab.classList.add('active'); brandTab.classList.remove('active'); showType(); }; + brandTab.onclick = () => { brandTab.classList.add('active'); typeTab.classList.remove('active'); showBrand(); }; + + async function showType() { + clear(body); + body.append(el('div', { class: 'muted' }, 'Loading…')); + let list = []; + try { list = await api.get('/api/icon-sets'); } catch { /* ignore */ } + clear(body); + for (const s of list) { + const grid = el('div', { class: 'ip-grid' }, s.icons.map(file => { + const name = file.replace(/\.[a-z]+$/, ''); + const ref = `set:${s.set}:${name}`; + const b = el('button', { class: 'ip-icon', title: name }, + el('img', { src: `/api/icon-sets/${s.set}/${file}` })); + b.onclick = () => onPick(ref); + return b; + })); + body.append(el('div', { class: 'ip-set' }, + el('div', { class: 'ip-set-hd' }, s.set + (s.readonly ? '' : ' ·')), + grid)); + } + } + + function showBrand() { + clear(body); + const inp = el('input', { class: 'dv-edit-name', placeholder: 'brand slug e.g. apple, google-nest' }); + const prev = el('div', { class: 'ip-grid' }); + inp.oninput = () => { + const slug = inp.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, ''); + clear(prev); + if (!slug) return; + const b = el('button', { class: 'ip-icon' }, + el('img', { src: `/api/icons/${slug}.png` })); + b.onclick = () => onPick(`brand:${slug}`); + prev.append(b); + }; + body.append(inp, prev); + } + + mount(tabs, typeTab, brandTab); + mount(box, tabs, body); + showType(); + return box; +}