diff --git a/templates/editor.html b/templates/editor.html index c5f798a..6e1c651 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -4,7 +4,7 @@ BFA Banana Dryer — HMI Design Tool - + @@ -193,12 +228,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
-
- - - - -
+ @@ -326,7 +356,7 @@ function setMode(m) { document.getElementById('card-grid').classList.toggle('edit-mode', m === 'edit'); document.getElementById('preview-tag').style.display = m === 'preview' ? 'inline' : 'none'; document.querySelectorAll('.edit-overlay').forEach(e => e.style.display = m === 'edit' ? 'flex' : 'none'); - document.getElementById('add-palette').style.display = m === 'edit' ? 'flex' : 'none'; + // add-palette removed — inline + buttons handle this now if (m === 'edit') initSortable(); else if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } } @@ -398,7 +428,35 @@ function renderCards() { const el = createCardEl(c); grid.appendChild(el); }); - if (mode === 'edit') initSortable(); + // In edit mode, fill remaining slots with + placeholders (up to 6 slots per page) + if (mode === 'edit') { + const halfCards = page.cards.filter(c => c.width !== 'full').length; + const fullCards = page.cards.filter(c => c.width === 'full').length; + const slotsUsed = halfCards + fullCards * 2; + const emptySlots = Math.max(1, 6 - slotsUsed); // always show at least 1 + for (let i = 0; i < emptySlots; i++) { + const ph = document.createElement('div'); + ph.className = 'add-placeholder'; + ph.innerHTML = `+ +
+ + + + + +
`; + ph.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON') return; + // Toggle the menu + const menu = ph.querySelector('.add-menu'); + document.querySelectorAll('.add-menu.open').forEach(m => { if(m!==menu) m.classList.remove('open'); }); + menu.classList.toggle('open'); + e.stopPropagation(); + }); + grid.appendChild(ph); + } + initSortable(); + } } function createCardEl(c) { @@ -413,6 +471,8 @@ function createCardEl(c) { // Edit buttons el.innerHTML += `
+ +
`; @@ -421,32 +481,219 @@ function createCardEl(c) { const commentCount = (layout.comments || []).filter(x => x.target === c.id).length; el.innerHTML += `
${commentCount}
`; + const cfs = c.fontSize || 18; if (c.type === 'temp') { const color = c.color || '#ff8844'; - el.innerHTML += `
${c.label}
`; + el.innerHTML += `
${c.label}
`; el.innerHTML += `
${Math.round(s.value)} °C
`; el.innerHTML += `
`; el.innerHTML += `
SP: ${c.sp_default || 70} °C
`; el.onclick = () => { if (mode === 'preview') openTempPopup(c); }; } else if (c.type === 'motor') { el.innerHTML += `
- ${c.label} + ${c.label} ${c.sp_default || 50}%
`; el.innerHTML += `
${s.on ? 'ON' : 'OFF'}
`; el.onclick = () => { if (mode === 'preview') toggleSim(c.id); }; } else if (c.type === 'burner') { - el.innerHTML += `
${c.label}
`; + el.innerHTML += `
${c.label}
`; el.innerHTML += `
${s.on ? 'ON' : 'OFF'}
`; el.onclick = () => { if (mode === 'preview') toggleSim(c.id); }; + } else if (c.type === 'automation') { + // Automation card — combines outputs, speeds, temp setpoints + el.innerHTML += `
${c.label}
`; + const rules = c.rules || []; + if (rules.length === 0) { + el.innerHTML += `
No rules configured
Click to edit in edit mode
`; + } else { + let rulesHtml = '
'; + rules.forEach(r => { + const icon = r.type === 'temp' ? '🌡' : r.type === 'motor' ? '⚙' : r.type === 'output' ? '⚡' : '●'; + rulesHtml += `
${icon} ${r.label || r.target}: ${r.action || ''} ${r.value !== undefined ? r.value : ''}
`; + }); + rulesHtml += '
'; + el.innerHTML += rulesHtml; + } + el.onclick = () => { + if (mode === 'edit') { event.stopPropagation(); editAutomation(c); } + else if (mode === 'preview') toggleSim(c.id); + }; } else { // output - el.innerHTML += `
${c.label}
`; + el.innerHTML += `
${c.label}
`; el.innerHTML += `
${s.on ? 'ON' : 'OFF'}
`; el.onclick = () => { if (mode === 'preview') toggleSim(c.id); }; } return el; } +let autoEditCard = null; + +function editAutomation(c) { + autoEditCard = c; + // Gather all available controls from the layout + const allControls = []; + layout.pages.forEach(p => p.cards.forEach(card => { + if (card.id !== c.id) allControls.push(card); + })); + + const popup = document.getElementById('popup'); + popup.style.borderColor = 'var(--border)'; + + let rulesHtml = ''; + (c.rules || []).forEach((r, i) => { + rulesHtml += buildRuleRow(i, r, allControls); + }); + + popup.innerHTML = ` +
+

Edit Automation: ${c.label}

+

+ When this automation is activated, all these actions happen at once. + Add what should turn on, set speeds, or adjust temperatures. +

+
${rulesHtml}
+ +
+ + +
+
`; + document.getElementById('popup-overlay').classList.add('active'); +} + +function buildRuleRow(idx, rule, controls) { + if (!controls) { + controls = []; + layout.pages.forEach(p => p.cards.forEach(c => { if(autoEditCard && c.id !== autoEditCard.id) controls.push(c); })); + } + + // Group controls by type + const temps = controls.filter(c => c.type === 'temp'); + const motors = controls.filter(c => c.type === 'motor'); + const outputs = controls.filter(c => c.type === 'output' || c.type === 'burner'); + + // What to control dropdown + let targetOpts = ''; + if (temps.length) { + targetOpts += ''; + temps.forEach(c => targetOpts += ``); + targetOpts += ''; + } + if (motors.length) { + targetOpts += ''; + motors.forEach(c => targetOpts += ``); + targetOpts += ''; + } + if (outputs.length) { + targetOpts += ''; + outputs.forEach(c => targetOpts += ``); + targetOpts += ''; + } + + // Action depends on type + const isTemp = rule.type === 'temp'; + const isMotor = rule.type === 'motor'; + const isOutput = rule.type === 'output' || rule.type === 'burner'; + + let actionOpts = ''; + if (isTemp) { + actionOpts = ``; + } else if (isMotor) { + actionOpts = ` + + + `; + } else { + actionOpts = ` + + `; + } + + const showValue = rule.action === 'set'; + const valueUnit = isTemp ? '°C' : '%'; + const valueMax = isTemp ? 200 : 100; + const valueStep = isTemp ? 1 : 5; + + return ` +
+ + + + + ${valueUnit} + + +
`; +} + +function autoRuleTargetChanged(sel, idx) { + const opt = sel.options[sel.selectedIndex]; + const type = opt.dataset.type || 'output'; + // Store temporarily so we can rebuild the action dropdown + const rules = collectAutoRules(); + if (rules[idx]) { + rules[idx].target = sel.value; + rules[idx].type = type; + rules[idx].label = opt.textContent; + // Reset action based on type + if (type === 'temp') rules[idx].action = 'set'; + else rules[idx].action = 'on'; + } + autoEditCard.rules = rules; + editAutomation(autoEditCard); // re-render the whole popup +} + +function autoRuleActionChanged(idx) { + const actionSel = document.querySelector(`.rule-action[data-idx="${idx}"]`); + const valueWrap = document.querySelectorAll('.rule-value-wrap')[idx]; + if (actionSel && valueWrap) { + valueWrap.style.display = actionSel.value === 'set' ? 'flex' : 'none'; + } +} + +function addAutoRule() { + if (!autoEditCard) return; + const rules = collectAutoRules(); + rules.push({type:'output', target:'', label:'', action:'on', value:0}); + autoEditCard.rules = rules; + editAutomation(autoEditCard); +} + +function removeAutoRule(idx) { + if (!autoEditCard) return; + const rules = collectAutoRules(); + rules.splice(idx, 1); + autoEditCard.rules = rules; + editAutomation(autoEditCard); +} + +function collectAutoRules() { + const rows = document.querySelectorAll('.auto-rule'); + const rules = []; + rows.forEach(row => { + const targetSel = row.querySelector('select'); + const actionSel = row.querySelector('.rule-action'); + const valueInput = row.querySelector('.rule-value'); + const opt = targetSel.options[targetSel.selectedIndex]; + rules.push({ + type: opt?.dataset?.type || 'output', + target: targetSel.value, + label: opt?.textContent || '', + action: actionSel?.value || 'on', + value: actionSel?.value === 'set' ? parseInt(valueInput?.value || 0) : undefined + }); + }); + return rules; +} + +function saveAutoRules() { + if (!autoEditCard) return; + autoEditCard.rules = collectAutoRules().filter(r => r.target); // remove empty rows + closePopup(); + renderCards(); +} + // ══════════════════════════════════════════════════════ // DRAG & DROP // ══════════════════════════════════════════════════════ @@ -469,14 +716,17 @@ function initSortable() { // CARD ACTIONS // ══════════════════════════════════════════════════════ function addCard(type) { + // Close any open add menus + document.querySelectorAll('.add-menu.open').forEach(m => m.classList.remove('open')); const id = type + '_' + Date.now(); const defaults = { temp: {label: 'New Temp', color: '#ff8844', sp_default: 70}, motor: {label: 'New Motor', sp_default: 50}, output: {label: 'New Output'}, - burner: {label: 'Burner'} + burner: {label: 'Burner'}, + automation: {label: 'New Automation', width: 'full', rules: []} }; - const card = {id, type, width: 'half', ...defaults[type]}; + const card = {id, type, width: defaults[type]?.width || 'half', ...defaults[type]}; layout.pages[currentPage].cards.push(card); simState[id] = {on: false, value: card.sp_default || 0}; renderCards(); @@ -490,12 +740,97 @@ function deleteCard(id) { renderCards(); } -function renameCard(id) { +function editCardProps(id) { + const card = findCard(id); + if (!card) return; + + const p = document.getElementById('popup'); + p.style.borderColor = 'transparent'; + + let typeOpts = ['temp','motor','output','burner','automation'].map(t => + `` + ).join(''); + + let extraFields = ''; + if (card.type === 'temp') { + extraFields = ` +
°C
`; + } else if (card.type === 'motor') { + extraFields = ` +
%
`; + } + + p.innerHTML = ` +
+

Edit Card

+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + + ${card.fontSize||18}px +
+ ${extraFields} +
+ + +
+
`; + + // Live preview for color picker + document.getElementById('prop-color').addEventListener('input', e => { + document.getElementById('prop-color-preview').style.background = e.target.value; + }); + document.getElementById('prop-fontsize').addEventListener('input', e => { + document.getElementById('prop-fontsize-val').textContent = e.target.value + 'px'; + }); + + document.getElementById('popup-overlay').classList.add('active'); +} + +function saveCardProps(id) { + const card = findCard(id); + if (!card) return; + + card.label = document.getElementById('prop-name').value || card.label; + card.type = document.getElementById('prop-type').value; + card.width = document.getElementById('prop-width').value; + const color = document.getElementById('prop-color').value; + card.color = color || null; + card.fontSize = parseInt(document.getElementById('prop-fontsize').value) || 18; + + const spEl = document.getElementById('prop-sp'); + if (spEl) card.sp_default = parseInt(spEl.value) || 0; + + closePopup(); + renderCards(); +} + +function toggleCardSize(id) { const page = layout.pages[currentPage]; const card = page.cards.find(c => c.id === id); if (!card) return; - const name = prompt('New name:', card.label); - if (name) { card.label = name; renderCards(); } + card.width = card.width === 'full' ? 'half' : 'full'; + renderCards(); } // ══════════════════════════════════════════════════════ @@ -504,6 +839,34 @@ function renameCard(id) { function toggleSim(id) { if (!simState[id]) simState[id] = {on: false, value: 0}; simState[id].on = !simState[id].on; + + // Burner turns on Hot Fan automatically + const card = findCard(id); + if (card && card.type === 'burner') { + const fanCard = findCardByLabel('Hot Fan'); + if (fanCard) { + if (!simState[fanCard.id]) simState[fanCard.id] = {on: false, value: fanCard.sp_default || 50}; + simState[fanCard.id].on = simState[id].on; + } + } + + // If this is an automation card, activate/deactivate all its rules + if (card && card.type === 'automation' && card.rules) { + card.rules.forEach(r => { + if (!r.target) return; + if (!simState[r.target]) simState[r.target] = {on: false, value: 0}; + if (simState[id].on) { + // Activating: apply all rules + if (r.action === 'on') simState[r.target].on = true; + else if (r.action === 'off') simState[r.target].on = false; + else if (r.action === 'set') { simState[r.target].on = true; simState[r.target].value = r.value || 0; } + } else { + // Deactivating: turn everything off + if (r.action !== 'off') simState[r.target].on = false; + } + }); + } + renderCards(); updateSchematicAnimations(); } @@ -540,6 +903,10 @@ function findCard(id) { for (const p of layout.pages) { const c = p.cards.find(x => x.id === id); if (c) return c; } return null; } +function findCardByLabel(label) { + for (const p of layout.pages) { const c = p.cards.find(x => x.label === label); if (c) return c; } + return null; +} // ══════════════════════════════════════════════════════ // COMMENTS @@ -654,7 +1021,10 @@ function hideCtx() { document.getElementById('eq-context').style.display = 'none'; ctxEquip = null; } -document.addEventListener('click', e => { if (!e.target.closest('#eq-context')) hideCtx(); }); +document.addEventListener('click', e => { + if (!e.target.closest('#eq-context')) hideCtx(); + if (!e.target.closest('.add-placeholder')) document.querySelectorAll('.add-menu.open').forEach(m => m.classList.remove('open')); +}); function ctxAction(action) { if (!ctxEquip) return;