diff --git a/server/src/wled_controller/static/css/components.css b/server/src/wled_controller/static/css/components.css index aa6b8ff..5f4f279 100644 --- a/server/src/wled_controller/static/css/components.css +++ b/server/src/wled_controller/static/css/components.css @@ -695,6 +695,80 @@ textarea:focus-visible { .icon-select-grid { gap: 4px; padding: 4px; } } +/* ── Type Picker Overlay ─────────────────────────────────── */ + +.type-picker-overlay { + position: fixed; + inset: 0; + z-index: 3000; + display: flex; + justify-content: center; + padding-top: 15vh; + background: rgba(0, 0, 0, 0); + backdrop-filter: blur(0px); + transition: background 0.2s, backdrop-filter 0.2s; +} +.type-picker-overlay.open { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); +} +.type-picker-dialog { + align-self: flex-start; + width: min(680px, 90vw); + max-height: 70vh; + overflow-y: auto; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 16px 48px var(--shadow-color); + padding: 16px; + opacity: 0; + transform: translateY(-12px) scale(0.98); + transition: opacity 0.2s, transform 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} +.type-picker-overlay.open .type-picker-dialog { + opacity: 1; + transform: translateY(0) scale(1); +} +.type-picker-title { + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-secondary); + margin-bottom: 12px; + text-align: center; +} +.type-picker-filter { + width: 100%; + padding: 8px 12px; + margin-bottom: 12px; + border: 1px solid var(--border-color); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-color); + font-size: 0.9rem; + outline: none; + box-sizing: border-box; +} +.type-picker-filter::placeholder { + color: var(--text-secondary); +} +.type-picker-filter:focus { + border-color: var(--primary-color); +} +.type-picker-dialog .icon-select-grid { + border: none; + background: none; + padding: 0; + /* Override inline columns — use responsive auto-fill */ + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important; +} +.type-picker-dialog .icon-select-cell.disabled { + opacity: 0.25; + pointer-events: none; +} + /* ── Entity Palette (command-palette style selector) ─────── */ .entity-palette-overlay { diff --git a/server/src/wled_controller/static/js/core/icon-select.js b/server/src/wled_controller/static/js/core/icon-select.js index 333fb0e..253cead 100644 --- a/server/src/wled_controller/static/js/core/icon-select.js +++ b/server/src/wled_controller/static/js/core/icon-select.js @@ -18,6 +18,8 @@ * Call sel.setValue(v) to change programmatically, sel.destroy() to remove. */ +import { desktopFocus } from './ui.js'; + const POPUP_CLASS = 'icon-select-popup'; /** Close every open icon-select popup. */ @@ -227,3 +229,79 @@ export class IconSelect { this._select.style.display = ''; } } + +/** + * Show a standalone type-picker overlay. + * + * Displays a centered modal with an icon grid. When the user picks a type, + * the overlay closes and `onPick(value)` is called. Clicking the backdrop + * or pressing Escape dismisses without picking. + * + * @param {Object} opts + * @param {string} opts.title - heading text + * @param {Array<{value:string, icon:string, label:string, desc?:string}>} opts.items + * @param {Function} opts.onPick - called with the selected value + */ +export function showTypePicker({ title, items, onPick }) { + const showFilter = items.length > 9; + + // Build cells + const cells = items.map(item => + `
+ ${item.icon} + ${item.label} + ${item.desc ? `${item.desc}` : ''} +
` + ).join(''); + + // Create overlay + const overlay = document.createElement('div'); + overlay.className = 'type-picker-overlay'; + overlay.innerHTML = ` +
+
${title}
+ ${showFilter ? '' : ''} +
${cells}
+
`; + document.body.appendChild(overlay); + + const close = () => { overlay.remove(); document.removeEventListener('keydown', onKey); }; + + // Filter logic + if (showFilter) { + const input = overlay.querySelector('.type-picker-filter'); + const allCells = overlay.querySelectorAll('.icon-select-cell'); + input.addEventListener('input', () => { + const q = input.value.toLowerCase().trim(); + allCells.forEach(cell => { + const match = !q || cell.dataset.search.includes(q); + cell.classList.toggle('disabled', !match); + }); + }); + // Auto-focus filter after animation (skip on touch devices to avoid keyboard popup) + requestAnimationFrame(() => setTimeout(() => desktopFocus(input), 200)); + } + + // Backdrop click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) close(); + }); + + // Escape key + const onKey = (e) => { + if (e.key === 'Escape') close(); + }; + document.addEventListener('keydown', onKey); + + // Cell clicks + overlay.querySelectorAll('.icon-select-cell').forEach(cell => { + cell.addEventListener('click', () => { + if (cell.classList.contains('disabled')) return; + close(); + onPick(cell.dataset.value); + }); + }); + + // Animate in + requestAnimationFrame(() => overlay.classList.add('open')); +} diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js index 06cc756..9407fb2 100644 --- a/server/src/wled_controller/static/js/features/color-strips.js +++ b/server/src/wled_controller/static/js/features/color-strips.js @@ -19,7 +19,7 @@ import * as P from '../core/icon-paths.js'; import { wrapCard } from '../core/card-colors.js'; import { TagInput, renderTagChips } from '../core/tag-input.js'; import { attachProcessPicker } from '../core/process-picker.js'; -import { IconSelect } from '../core/icon-select.js'; +import { IconSelect, showTypePicker } from '../core/icon-select.js'; import { EntitySelect } from '../core/entity-palette.js'; import { rgbArrayToHex, hexToRgbArray, @@ -1315,7 +1315,16 @@ function _autoGenerateCSSName() { /* ── Editor open/close ────────────────────────────────────────── */ -export async function showCSSEditor(cssId = null, cloneData = null) { +export async function showCSSEditor(cssId = null, cloneData = null, presetType = null) { + // When creating new: show type picker first, then re-enter with presetType + if (!cssId && !cloneData && !presetType) { + showTypePicker({ + title: t('color_strip.select_type'), + items: _buildCSSTypeItems(), + onPick: (type) => showCSSEditor(null, null, type), + }); + return; + } try { const sources = await streamsCache.fetch(); @@ -1449,8 +1458,8 @@ export async function showCSSEditor(cssId = null, cloneData = null) { // Initialize icon-grid type selector (idempotent) _ensureCSSTypeIconSelect(); - // Hide type selector in edit mode (type is immutable) - document.getElementById('css-editor-type-group').style.display = cssId ? 'none' : ''; + // Hide type selector — type is chosen before the modal opens (or immutable in edit) + document.getElementById('css-editor-type-group').style.display = 'none'; if (cssId) { const cssSources = await colorStripSourcesCache.fetch(); @@ -1481,7 +1490,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) { } else { document.getElementById('css-editor-id').value = ''; document.getElementById('css-editor-name').value = ''; - document.getElementById('css-editor-type').value = 'picture'; + document.getElementById('css-editor-type').value = presetType || 'picture'; onCSSTypeChange(); document.getElementById('css-editor-interpolation').value = 'average'; if (_interpolationIconSelect) _interpolationIconSelect.setValue('average'); @@ -1527,7 +1536,8 @@ export async function showCSSEditor(cssId = null, cloneData = null) { document.getElementById('css-editor-candlelight-num-candles').value = 3; document.getElementById('css-editor-candlelight-speed').value = 1.0; document.getElementById('css-editor-candlelight-speed-val').textContent = '1.0'; - document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.add')}`; + const typeIcon = getColorStripIcon(presetType || 'picture'); + document.getElementById('css-editor-title').innerHTML = `${typeIcon} ${t('color_strip.add')}: ${t(`color_strip.type.${presetType || 'picture'}`)}`; document.getElementById('css-editor-gradient-preset').value = ''; gradientInit([ { position: 0.0, color: [255, 0, 0] }, diff --git a/server/src/wled_controller/static/js/features/device-discovery.js b/server/src/wled_controller/static/js/features/device-discovery.js index d3890c1..b0d69be 100644 --- a/server/src/wled_controller/static/js/features/device-discovery.js +++ b/server/src/wled_controller/static/js/features/device-discovery.js @@ -13,7 +13,7 @@ import { showToast, desktopFocus } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { _computeMaxFps, _renderFpsHint } from './devices.js'; import { getDeviceTypeIcon, ICON_RADIO, ICON_GLOBE, ICON_CPU, ICON_KEYBOARD, ICON_MOUSE, ICON_HEADPHONES, ICON_PLUG, ICON_TARGET_ICON, ICON_ACTIVITY } from '../core/icons.js'; -import { IconSelect } from '../core/icon-select.js'; +import { IconSelect, showTypePicker } from '../core/icon-select.js'; class AddDeviceModal extends Modal { constructor() { super('add-device-modal'); } @@ -262,6 +262,7 @@ export function onDeviceTypeChanged() { opt.value = ''; opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...'; opt.disabled = true; + opt.selected = true; serialSelect.appendChild(opt); } updateBaudFpsHint(); @@ -328,6 +329,7 @@ export function onDeviceTypeChanged() { opt.value = ''; opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...'; opt.disabled = true; + opt.selected = true; serialSelect.appendChild(opt); } } else if (isHueDevice(deviceType)) { @@ -525,6 +527,14 @@ function _populateSerialPortDropdown(devices) { return; } + // Default placeholder + const placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = t('device.serial_port.select') || 'Select a port...'; + placeholder.disabled = true; + placeholder.selected = true; + select.appendChild(placeholder); + devices.forEach(device => { const opt = document.createElement('option'); opt.value = device.url; @@ -544,7 +554,17 @@ export function onSerialPortFocus() { } } -export function showAddDevice() { +export function showAddDevice(presetType = null) { + // When no type specified: show type picker first + if (!presetType) { + showTypePicker({ + title: t('device.select_type'), + items: _buildDeviceTypeItems(), + onPick: (type) => showAddDevice(type), + }); + return; + } + const form = document.getElementById('add-device-form'); const error = document.getElementById('add-device-error'); form.reset(); @@ -563,6 +583,14 @@ export function showAddDevice() { const scanBtn = document.getElementById('scan-network-btn'); if (scanBtn) scanBtn.disabled = false; _ensureDeviceTypeIconSelect(); + + // Pre-select type and hide the type selector (already chosen) + document.getElementById('device-type').value = presetType; + document.getElementById('device-type-group').style.display = 'none'; + const typeIcon = getDeviceTypeIcon(presetType); + const typeName = t(`device.type.${presetType}`); + document.getElementById('add-device-modal-title').innerHTML = `${typeIcon} ${t('devices.add')}: ${typeName}`; + addDeviceModal.open(); onDeviceTypeChanged(); setTimeout(() => { diff --git a/server/src/wled_controller/static/js/features/value-sources.js b/server/src/wled_controller/static/js/features/value-sources.js index 0a6ea3b..f95ca86 100644 --- a/server/src/wled_controller/static/js/features/value-sources.js +++ b/server/src/wled_controller/static/js/features/value-sources.js @@ -23,7 +23,7 @@ import { } from '../core/icons.js'; import { wrapCard } from '../core/card-colors.js'; import { TagInput, renderTagChips } from '../core/tag-input.js'; -import { IconSelect } from '../core/icon-select.js'; +import { IconSelect, showTypePicker } from '../core/icon-select.js'; import { EntitySelect } from '../core/entity-palette.js'; import { loadPictureSources } from './streams.js'; @@ -165,13 +165,27 @@ function _ensureVSTypeIconSelect() { // ── Modal ───────────────────────────────────────────────────── -export async function showValueSourceModal(editData) { +export async function showValueSourceModal(editData, presetType = null) { + // When creating new: show type picker first, then re-enter with presetType + if (!editData && !presetType) { + showTypePicker({ + title: t('value_source.select_type'), + items: _buildVSTypeItems(), + onPick: (type) => showValueSourceModal(null, type), + }); + return; + } + const hasId = editData?.id; const isEdit = !!hasId; - const titleKey = isEdit ? 'value_source.edit' : 'value_source.add'; - const titleIcon = editData ? getValueSourceIcon(editData.source_type) : getValueSourceIcon('static'); - document.getElementById('value-source-modal-title').innerHTML = `${titleIcon} ${t(titleKey)}`; + const sourceType = editData?.source_type || presetType || 'static'; + const titleIcon = getValueSourceIcon(sourceType); + const titleKey = isEdit ? 'value_source.edit' : 'value_source.add'; + const typeName = t(`value_source.type.${sourceType}`); + document.getElementById('value-source-modal-title').innerHTML = isEdit + ? `${titleIcon} ${t(titleKey)}` + : `${titleIcon} ${t(titleKey)}: ${typeName}`; document.getElementById('value-source-id').value = isEdit ? editData.id : ''; document.getElementById('value-source-error').style.display = 'none'; @@ -180,7 +194,8 @@ export async function showValueSourceModal(editData) { _ensureVSTypeIconSelect(); const typeSelect = document.getElementById('value-source-type'); - document.getElementById('value-source-type-group').style.display = isEdit ? 'none' : ''; + // Type is chosen before the modal opens — always hide selector + document.getElementById('value-source-type-group').style.display = 'none'; if (editData) { document.getElementById('value-source-name').value = editData.name || ''; @@ -227,7 +242,7 @@ export async function showValueSourceModal(editData) { } else { document.getElementById('value-source-name').value = ''; document.getElementById('value-source-description').value = ''; - typeSelect.value = 'static'; + typeSelect.value = presetType || 'static'; onValueSourceTypeChange(); _setSlider('value-source-value', 1.0); _setSlider('value-source-speed', 10); diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index cbf68ac..61b5ddd 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -112,6 +112,7 @@ "templates.test.error.no_display": "Please select a display", "templates.test.error.failed": "Test failed", "devices.title": "Devices", + "device.select_type": "Select Device Type", "devices.add": "Add New Device", "devices.loading": "Loading devices...", "devices.none": "No devices configured", @@ -211,6 +212,7 @@ "device.serial_port": "Serial Port:", "device.serial_port.hint": "Select the COM port of the Adalight device", "device.serial_port.none": "No serial ports found", + "device.serial_port.select": "Select a port...", "device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)", "device.baud_rate": "Baud Rate:", "device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.", @@ -827,6 +829,7 @@ "aria.previous": "Previous", "aria.next": "Next", "aria.hint": "Show hint", + "color_strip.select_type": "Select Color Strip Type", "color_strip.add": "Add Color Strip Source", "color_strip.edit": "Edit Color Strip Source", "color_strip.name": "Name:", @@ -1179,6 +1182,7 @@ "tree.group.picture": "Picture", "tree.group.utility": "Utility", "value_source.group.title": "Value Sources", + "value_source.select_type": "Select Value Source Type", "value_source.add": "Add Value Source", "value_source.edit": "Edit Value Source", "value_source.name": "Name:", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 40b6a62..8b8bf5d 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -112,6 +112,7 @@ "templates.test.error.no_display": "Пожалуйста, выберите дисплей", "templates.test.error.failed": "Тест не удался", "devices.title": "Устройства", + "device.select_type": "Выберите тип устройства", "devices.add": "Добавить Новое Устройство", "devices.loading": "Загрузка устройств...", "devices.none": "Устройства не настроены", @@ -160,6 +161,7 @@ "device.serial_port": "Серийный порт:", "device.serial_port.hint": "Выберите COM порт устройства Adalight", "device.serial_port.none": "Серийные порты не найдены", + "device.serial_port.select": "Выберите порт...", "device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)", "device.baud_rate": "Скорость порта:", "device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.", @@ -776,6 +778,7 @@ "aria.previous": "Назад", "aria.next": "Вперёд", "aria.hint": "Показать подсказку", + "color_strip.select_type": "Выберите тип цветовой полосы", "color_strip.add": "Добавить источник цветовой полосы", "color_strip.edit": "Редактировать источник цветовой полосы", "color_strip.name": "Название:", @@ -1128,6 +1131,7 @@ "tree.group.picture": "Изображения", "tree.group.utility": "Утилиты", "value_source.group.title": "Источники значений", + "value_source.select_type": "Выберите тип источника значений", "value_source.add": "Добавить источник значений", "value_source.edit": "Редактировать источник значений", "value_source.name": "Название:", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 37de2aa..77a73ac 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -112,6 +112,7 @@ "templates.test.error.no_display": "请选择显示器", "templates.test.error.failed": "测试失败", "devices.title": "设备", + "device.select_type": "选择设备类型", "devices.add": "添加新设备", "devices.loading": "正在加载设备...", "devices.none": "尚未配置设备", @@ -160,6 +161,7 @@ "device.serial_port": "串口:", "device.serial_port.hint": "选择 Adalight 设备的 COM 端口", "device.serial_port.none": "未找到串口", + "device.serial_port.select": "选择端口...", "device.led_count_manual.hint": "灯带上的 LED 数量(必须与 Arduino 程序匹配)", "device.baud_rate": "波特率:", "device.baud_rate.hint": "串口通信速率。越高 FPS 越高,但需要与 Arduino 程序匹配。", @@ -776,6 +778,7 @@ "aria.previous": "上一个", "aria.next": "下一个", "aria.hint": "显示提示", + "color_strip.select_type": "选择色带类型", "color_strip.add": "添加色带源", "color_strip.edit": "编辑色带源", "color_strip.name": "名称:", @@ -1128,6 +1131,7 @@ "tree.group.picture": "图片", "tree.group.utility": "工具", "value_source.group.title": "值源", + "value_source.select_type": "选择值源类型", "value_source.add": "添加值源", "value_source.edit": "编辑值源", "value_source.name": "名称:", diff --git a/server/src/wled_controller/templates/modals/add-device.html b/server/src/wled_controller/templates/modals/add-device.html index c80f032..3d875e1 100644 --- a/server/src/wled_controller/templates/modals/add-device.html +++ b/server/src/wled_controller/templates/modals/add-device.html @@ -20,7 +20,7 @@
-
+