bfa-dryer-design/templates/editor.html

334 lines
16 KiB
HTML
Raw Permalink 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}
/* Main layout — editor + optional sim panel side by side */
.main-wrap{display:flex;height:calc(100vh - 48px)}
.editor-wrap{flex:1;overflow:hidden}
.editor-wrap #gjs{height:100% !important}
/* Simulation toggle panel */
.sim-panel{width:0;overflow:hidden;background:#080c14;border-left:2px solid var(--border);transition:width 0.2s;flex-shrink:0}
.sim-panel.open{width:45%}
.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>
<!-- MAIN LAYOUT -->
<div class="main-wrap">
<div class="editor-wrap">
<div id="gjs"></div>
</div>
<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>
</div><!-- /main-wrap -->
<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: ''},
]
},
styleManager: {
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'}]},
]
}
]
},
blockManager: {blocks: []},
canvas: {
styles: ['static/hmi-canvas.css'],
}
});
// Use GrapesJS default panels — they include blocks, styles, layers, traits
// Just add device switching buttons
editor.Panels.addButton('options', {
id: 'device-tab5', command: e => e.setDevice('tab5'), label: 'Tab5', attributes: {title: 'Tab5 1280x720'}
});
editor.Panels.addButton('options', {
id: 'device-schneider', command: e => e.setDevice('schneider'), label: 'HMIDT', attributes: {title: 'Schneider 1280x800'}
});
editor.Panels.addButton('options', {
id: 'device-full', command: e => e.setDevice('desktop'), label: 'Full', attributes: {title: 'Full width'}
});
// ══════════════════════════════════════════════════════
// Register HMI blocks
// ══════════════════════════════════════════════════════
registerHMIBlocks(editor);
// Open blocks panel by default
editor.on('load', () => {
const pn = editor.Panels;
const openBl = pn.getButton('views', 'open-blocks');
if (openBl) openBl.set('active', true);
});
// ══════════════════════════════════════════════════════
// 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();
if (data.assets || data.styles || (data.pages && data.pages.length && data.pages[0].frames)) {
// V2 GrapesJS project — load it
editor.loadProjectData(data);
} else {
// No V2 yet — try loading converted V1 HTML
try {
const htmlResp = await fetch('api/v1html');
const html = await htmlResp.text();
if (html && html.length > 10) {
editor.setComponents(html);
console.log('Loaded V1 converted layout');
}
} catch(e2) { console.log('No V1 HTML either, starting blank'); }
}
} 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');
// Give GrapesJS time to see the resize, then refresh canvas
setTimeout(() => editor.refresh(), 300);
}
// Inject page-switching script into the canvas after components load
editor.on('load', () => {
// Add script to make tab clicks switch pages inside the canvas
const script = `
document.addEventListener('click', function(e) {
var tab = e.target.closest('[style*="cursor:pointer"][style*="font-weight:600"]');
if (!tab || !tab.parentElement || tab.parentElement.children.length < 2) return;
// Check if it's in the tab bar
var bar = tab.parentElement;
if (!bar.style.cssText.includes('height:50px') && !bar.style.cssText.includes('height: 50px')) return;
// Get tab index
var tabs = Array.from(bar.children);
var idx = tabs.indexOf(tab);
if (idx < 0) return;
// Reset all tab styles
tabs.forEach(function(t) { t.style.color = '#7a8baa'; t.style.borderBottom = 'none'; });
tab.style.color = '#2d7ff9';
tab.style.borderBottom = '3px solid #2d7ff9';
// Show/hide pages
var pages = document.querySelectorAll('[data-page], [style*="flex-wrap:wrap"][style*="padding:8px"]');
var pageContainers = Array.from(pages).filter(function(p) { return p.style.cssText.includes('wrap') && p.style.cssText.includes('padding'); });
pageContainers.forEach(function(p, i) {
p.style.display = (i === idx) ? 'flex' : 'none';
});
});
`;
// Inject into canvas
const frame = editor.Canvas.getFrameEl();
if (frame && frame.contentDocument) {
const s = frame.contentDocument.createElement('script');
s.textContent = script;
frame.contentDocument.body.appendChild(s);
}
});
</script>
</body>
</html>