bfa-dryer-design/templates/editor.html
Richard Sauer 1831759563 V2: GrapesJS-based HMI design editor
Complete rewrite of the editor frontend using GrapesJS:
- Full drag/drop visual editor with style manager
- Custom HMI blocks: Temperature, Motor, Output, Burner, Automation, Gauge
- Layout blocks: Page Container, Tab Bar, Top Bar, Label, Divider, Spacer
- Style panel: font family/size/weight/color, background, border, layout, effects
- Traits panel: active/inactive colors, animation, linked control, setpoints
- Device manager: Tab5 (1280x720), Schneider HMIDT651 (1280x800), Desktop
- Theme switcher: Dark Industrial, Light Industrial, High Contrast, Classic SCADA
- Layers panel with z-index reorder and show/hide
- Undo/redo, preview mode, canvas zoom
- V1 layout preserved (saved as current.json), V2 uses current_v2.json
- Login/users/comments backend unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:06:01 +10:00

309 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>BFA Banana Dryer — HMI Design Tool</title>
<link rel="stylesheet" href="static/vendor/grapes.min.css">
<style>
:root{--bg:#0a0e17;--card:#131a2b;--border:#1e2a45;--text:#e8ecf4;--text2:#7a8baa;--dim:#4a5670;--blue:#2d7ff9;--green:#00c853;--amber:#ffab00;--red:#ff1744}
body{margin:0;font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden}
/* GrapesJS dark theme overrides */
.gjs-one-bg{background:var(--bg) !important}
.gjs-two-color{color:var(--text) !important}
.gjs-three-bg{background:var(--card) !important}
.gjs-four-color,.gjs-four-color-h:hover{color:var(--blue) !important}
.gjs-pn-panel{background:var(--card) !important;border-color:var(--border) !important}
.gjs-pn-views-container,.gjs-pn-views{background:var(--card) !important}
.gjs-block{background:var(--bg) !important;border:1px solid var(--border) !important;color:var(--text2) !important;border-radius:8px !important;min-height:60px !important}
.gjs-block:hover{border-color:var(--blue) !important;color:var(--text) !important}
.gjs-block__media{color:var(--text2) !important}
.gjs-blocks-cs{background:var(--card) !important}
.gjs-category-title,.gjs-layer-title,.gjs-sm-sector-title{background:var(--bg) !important;color:var(--text2) !important;border-color:var(--border) !important}
.gjs-field{background:var(--bg) !important;color:var(--text) !important;border-color:var(--border) !important}
.gjs-field input,.gjs-field select,.gjs-field textarea{color:var(--text) !important}
.gjs-sm-property,.gjs-trt-trait{color:var(--text2) !important}
.gjs-clm-tags .gjs-sm-composite{background:var(--bg) !important}
.gjs-layer{background:var(--card) !important;color:var(--text2) !important}
.gjs-layer.gjs-selected{background:var(--border) !important}
.gjs-input-holder,.gjs-sm-input-holder{background:var(--bg) !important}
.gjs-pn-btn{color:var(--text2) !important}
.gjs-pn-btn.gjs-pn-active{color:var(--blue) !important}
.gjs-cv-canvas{background:#080c14 !important}
.gjs-frame-wrapper{background:var(--bg) !important}
.gjs-toolbar{background:var(--card) !important}
.gjs-toolbar-item{color:var(--text) !important}
.gjs-resizer-h{border-color:var(--blue) !important}
.gjs-highlighter{outline-color:var(--blue) !important}
.gjs-badge{background:var(--blue) !important}
.gjs-ghost{background:rgba(45,127,249,0.15) !important;border:2px dashed var(--blue) !important}
/* Top bar */
.top-bar{display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 16px;background:#111827;border-bottom:1px solid var(--border);flex-shrink:0;z-index:10}
.top-bar .title{font-size:18px;font-weight:700}
.top-bar .sub{font-size:13px;color:var(--text2);margin-left:16px}
.top-bar-right{display:flex;align-items:center;gap:12px}
.top-bar select,.top-bar button{padding:5px 12px;border:1px solid var(--border);border-radius:5px;background:var(--card);color:var(--text);font-size:12px;cursor:pointer}
.top-bar button:hover{background:var(--border)}
.top-bar .save-btn{background:var(--green);border-color:var(--green);color:#000;font-weight:600}
/* Editor container */
#gjs{height:calc(100vh - 48px);width:100%}
/* Right panel width */
.gjs-pn-views-container{width:280px !important}
/* Simulation toggle panel */
.sim-panel{position:fixed;right:0;top:48px;width:50%;height:calc(100vh - 48px);background:#080c14;border-left:2px solid var(--border);z-index:20;display:none;overflow:auto}
.sim-panel.open{display:block}
.sim-panel-header{padding:8px 12px;background:var(--card);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;font-size:13px;font-weight:600;color:var(--text2)}
.sim-panel-header button{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:var(--card);color:var(--text2);font-size:11px;cursor:pointer}
</style>
</head>
<body>
<!-- TOP BAR -->
<div class="top-bar">
<div style="display:flex;align-items:center">
<span class="title">BFA Banana Dryer</span>
<span class="sub">HMI Design Tool v2</span>
</div>
<div class="top-bar-right">
<span style="color:var(--blue);font-weight:600">{{ user }}</span>
<a href="logout" style="color:var(--dim);text-decoration:none;font-size:12px">Logout</a>
<span style="color:var(--dim)">|</span>
<select id="theme-select" onchange="switchTheme(this.value)">
<option value="dark-industrial">Dark Industrial</option>
<option value="light-industrial">Light Industrial</option>
<option value="high-contrast">High Contrast</option>
<option value="classic-scada">Classic SCADA</option>
</select>
<button onclick="toggleSim()">Simulation</button>
<button class="save-btn" onclick="saveProject()">Save</button>
<button onclick="exportProject()">Export</button>
</div>
</div>
<!-- GRAPESJS EDITOR -->
<div id="gjs"></div>
<!-- SIMULATION PANEL (toggle) -->
<div class="sim-panel" id="sim-panel">
<div class="sim-panel-header">
MACHINE SIMULATION
<button onclick="toggleSim()">Close</button>
</div>
<div id="sim-container" style="padding:16px;display:flex;align-items:center;justify-content:center;min-height:400px"></div>
</div>
<script src="static/vendor/grapes.min.js"></script>
<script src="static/hmi-blocks.js"></script>
<script src="static/hmi-themes.js"></script>
<script>
// ══════════════════════════════════════════════════════
// GrapesJS Init
// ══════════════════════════════════════════════════════
const editor = grapesjs.init({
container: '#gjs',
fromElement: false,
height: '100%',
width: 'auto',
storageManager: {
type: 'remote',
stepsBeforeSave: 3,
options: {
remote: {
urlLoad: 'api/layout',
urlStore: 'api/layout',
fetchOptions: opts => ({...opts, method: opts.method || 'POST', headers: {'Content-Type':'application/json'}}),
onStore: data => data,
onLoad: result => result,
}
}
},
deviceManager: {
devices: [
{id: 'tab5', name: 'Tab5 (1280×720)', width: '1280px', height: '720px'},
{id: 'schneider', name: 'Schneider HMIDT651 (1280×800)', width: '1280px', height: '800px'},
{id: 'desktop', name: 'Desktop (Full)', width: ''},
]
},
panels: {defaults: []},
layerManager: {appendTo: '.gjs-pn-views-container'},
selectorManager: {appendTo: '.gjs-pn-views-container'},
styleManager: {
appendTo: '.gjs-pn-views-container',
sectors: [
{
name: 'Typography',
open: true,
properties: [
{property: 'font-family', type: 'select', defaults: 'Segoe UI',
list: [{value:'Segoe UI',name:'Segoe UI'},{value:'Arial',name:'Arial'},{value:'Helvetica',name:'Helvetica'},{value:'Roboto',name:'Roboto'},{value:'monospace',name:'Monospace'},{value:'Courier New',name:'Courier New'}]},
{property: 'font-size', type: 'number', units: ['px','em','rem'], defaults: '18px', min: 8, max: 72},
{property: 'font-weight', type: 'select', defaults: '400',
list: [{value:'400',name:'Normal'},{value:'600',name:'Semi Bold'},{value:'700',name:'Bold'},{value:'900',name:'Black'}]},
{property: 'color', type: 'color'},
{property: 'text-align', type: 'select', defaults: 'center',
list: [{value:'left',name:'Left'},{value:'center',name:'Center'},{value:'right',name:'Right'}]},
{property: 'line-height', type: 'number', units: ['px',''], defaults: '1.4'},
]
},
{
name: 'Background & Colors',
open: true,
properties: [
{property: 'background-color', type: 'color'},
{property: 'border-color', type: 'color'},
{property: 'border-width', type: 'number', units: ['px'], defaults: '1px', min: 0, max: 10},
{property: 'border-radius', type: 'number', units: ['px'], defaults: '10px', min: 0, max: 50},
{property: 'border-style', type: 'select', defaults: 'solid',
list: [{value:'none'},{value:'solid'},{value:'dashed'},{value:'dotted'}]},
{property: 'opacity', type: 'number', defaults: 1, min: 0, max: 1, step: 0.1},
]
},
{
name: 'Layout',
open: false,
properties: [
{property: 'width', type: 'number', units: ['px','%','vw']},
{property: 'height', type: 'number', units: ['px','%','vh']},
{property: 'min-height', type: 'number', units: ['px']},
{property: 'padding', type: 'composite',
properties: [{property:'padding-top',type:'number',units:['px']},{property:'padding-right',type:'number',units:['px']},{property:'padding-bottom',type:'number',units:['px']},{property:'padding-left',type:'number',units:['px']}]},
{property: 'margin', type: 'composite',
properties: [{property:'margin-top',type:'number',units:['px']},{property:'margin-right',type:'number',units:['px']},{property:'margin-bottom',type:'number',units:['px']},{property:'margin-left',type:'number',units:['px']}]},
{property: 'display', type: 'select', defaults: 'flex',
list: [{value:'block'},{value:'flex'},{value:'grid'},{value:'inline-block'},{value:'none'}]},
{property: 'flex-direction', type: 'select',
list: [{value:'row'},{value:'column'},{value:'row-reverse'},{value:'column-reverse'}]},
{property: 'justify-content', type: 'select',
list: [{value:'flex-start'},{value:'center'},{value:'flex-end'},{value:'space-between'},{value:'space-around'}]},
{property: 'align-items', type: 'select',
list: [{value:'flex-start'},{value:'center'},{value:'flex-end'},{value:'stretch'}]},
{property: 'gap', type: 'number', units: ['px'], defaults: '8px'},
{property: 'z-index', type: 'number', defaults: 'auto', min: -10, max: 100},
{property: 'overflow', type: 'select', list: [{value:'visible'},{value:'hidden'},{value:'auto'},{value:'scroll'}]},
]
},
{
name: 'Effects',
open: false,
properties: [
{property: 'box-shadow', type: 'text'},
{property: 'transition', type: 'text'},
{property: 'cursor', type: 'select', list: [{value:'default'},{value:'pointer'},{value:'grab'},{value:'not-allowed'}]},
]
}
]
},
traitManager: {appendTo: '.gjs-pn-views-container'},
blockManager: {
appendTo: '.gjs-pn-views-container',
blocks: []
},
canvas: {
styles: ['static/hmi-canvas.css'],
}
});
// ══════════════════════════════════════════════════════
// Panels: Views (Blocks, Styles, Layers, Traits)
// ══════════════════════════════════════════════════════
editor.Panels.addPanel({
id: 'views',
el: '.gjs-pn-views',
buttons: [
{id: 'open-blocks', command: 'open-blocks', active: true, label: 'Blocks', attributes: {title: 'HMI Components'}},
{id: 'open-sm', command: 'open-sm', label: 'Style', attributes: {title: 'Style Manager'}},
{id: 'open-layers', command: 'open-layers', label: 'Layers', attributes: {title: 'Layers'}},
{id: 'open-tm', command: 'open-tm', label: 'Traits', attributes: {title: 'Component Settings'}},
]
});
// Device buttons
editor.Panels.addPanel({
id: 'devices',
el: '.gjs-pn-devices-c',
buttons: [
{id: 'device-tab5', command: e => e.setDevice('tab5'), label: 'Tab5', active: true},
{id: 'device-schneider', command: e => e.setDevice('schneider'), label: 'Schneider'},
{id: 'device-desktop', command: e => e.setDevice('desktop'), label: 'Full'},
]
});
// Toolbar commands
editor.Panels.addPanel({
id: 'basic-actions',
el: '.gjs-pn-panel .gjs-pn-commands',
buttons: [
{id: 'undo', command: 'core:undo', label: 'Undo'},
{id: 'redo', command: 'core:redo', label: 'Redo'},
{id: 'preview', command: 'core:preview', label: 'Preview'},
{id: 'clear', command: 'core:canvas-clear', label: 'Clear'},
]
});
// ══════════════════════════════════════════════════════
// Register HMI blocks
// ══════════════════════════════════════════════════════
registerHMIBlocks(editor);
// ══════════════════════════════════════════════════════
// Save / Load / Export
// ══════════════════════════════════════════════════════
async function saveProject() {
try {
const data = editor.getProjectData();
await fetch('api/layout', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
alert('Saved!');
} catch(e) { alert('Save failed: ' + e.message); }
}
function exportProject() {
const data = editor.getProjectData();
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'bfa-hmi-design.json';
a.click();
}
// Load on init
(async function() {
try {
const resp = await fetch('api/layout');
const data = await resp.json();
// Check if it's V2 (GrapesJS) or V1 (old format)
if (data.assets || data.styles || data.pages) {
editor.loadProjectData(data);
} else {
// V1 layout — start fresh with default canvas content
console.log('V1 layout detected — starting fresh GrapesJS project');
}
} catch(e) { console.log('No saved layout, starting fresh'); }
})();
// ══════════════════════════════════════════════════════
// Theme switching
// ══════════════════════════════════════════════════════
function switchTheme(theme) {
applyHMITheme(editor, theme);
}
// ══════════════════════════════════════════════════════
// Simulation panel toggle
// ══════════════════════════════════════════════════════
function toggleSim() {
document.getElementById('sim-panel').classList.toggle('open');
}
</script>
</body>
</html>