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 += ``;
+ 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 += '';
+ }
+ if (motors.length) {
+ targetOpts += '';
+ }
+ if (outputs.length) {
+ 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;