279 lines
12 KiB
HTML
279 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Crumbblocks – Blockly ↔ Crumbforest</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<!-- Blockly + Python-Generator + Deutsch -->
|
||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||
<script src="https://unpkg.com/blockly/python.min.js"></script>
|
||
<script src="https://unpkg.com/blockly/msg/de.js"></script>
|
||
<style>
|
||
:root{
|
||
--bg:#0d0f12;--panel:#12151b;--ink:#e9edf5;--muted:#b7c0d0;--accent:#4caf50;--danger:#e74c3c;
|
||
--mono:ui-monospace, Menlo, Monaco, "Courier New", monospace;
|
||
}
|
||
html,body{height:100%;margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono)}
|
||
header{position:sticky;top:0;z-index:5;display:flex;gap:.6rem;align-items:center;padding:.6rem 1rem;background:#0b0d10cc;border-bottom:1px solid #1b212c}
|
||
header h2{margin:0;font-size:1rem;color:var(--muted)}
|
||
.spacer{flex:1 1 auto}
|
||
.btn{border:1px solid #263041;background:#161b22;color:var(--ink);border-radius:8px;padding:.5rem .75rem;cursor:pointer;font-weight:700}
|
||
.btn:hover{filter:brightness(1.1)}
|
||
.btn.primary{background:var(--accent);color:#0b130c;border-color:#2b6b2e}
|
||
.btn.danger{background:var(--danger);border-color:#b13a2e}
|
||
.btn.ghost{background:transparent}
|
||
#wrap{display:flex;flex-direction:column;gap:.6rem;height:calc(100% - 54px);padding:.6rem 1rem 1rem}
|
||
#blocklyDiv{height:62vh;width:100%;border-radius:10px;overflow:hidden;background:#0f1319;border:1px solid #1b212c}
|
||
#console{display:grid;grid-template-rows:auto 1fr;gap:.4rem;height:28vh;background:var(--panel);border:1px solid #1b212c;border-radius:10px}
|
||
#consoleHead{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border-bottom:1px solid #1b212c}
|
||
#statusDot{width:.6rem;height:.6rem;border-radius:50%;background:#6a6f79;display:inline-block}
|
||
#statusText{color:var(--muted);font-size:.9rem}
|
||
#output{margin:0;padding:.6rem .8rem;overflow:auto;line-height:1.35;color:#a6ffad;background:#0c0f13;border-radius:0 0 10px 10px;white-space:pre-wrap;word-break:break-word;font-size:.93rem}
|
||
@media (max-width:960px){#blocklyDiv{height:56vh}#console{height:32vh}}
|
||
.blocklyMainBackground{fill:#0f1319 !important}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h2>🧁 Crumbblocks</h2>
|
||
<div class="spacer"></div>
|
||
<button class="btn" id="btnDemo">Demo laden</button>
|
||
<button class="btn ghost" id="btnZoomOut">–</button>
|
||
<button class="btn ghost" id="btnZoomIn">+</button>
|
||
<button class="btn danger" id="btnTrash">Mülleimer</button>
|
||
<button class="btn primary" id="btnRun">Ausführen → Terminal</button>
|
||
</header>
|
||
|
||
<div id="wrap">
|
||
<div id="blocklyDiv" aria-label="Blockly Arbeitsfläche"></div>
|
||
|
||
<!-- Toolbox -->
|
||
<xml id="toolbox" style="display:none">
|
||
<category name="Logik" colour="#66b1ff">
|
||
<block type="controls_if"></block>
|
||
<block type="logic_compare"></block>
|
||
<block type="logic_operation"></block>
|
||
<block type="logic_boolean"></block>
|
||
</category>
|
||
<category name="Schleifen" colour="#6cdf6c">
|
||
<block type="controls_repeat_ext">
|
||
<value name="TIMES"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
|
||
</block>
|
||
<block type="controls_whileUntil"></block>
|
||
</category>
|
||
<category name="Mathe" colour="#b68cff">
|
||
<block type="math_number"></block>
|
||
<block type="math_arithmetic"></block>
|
||
</category>
|
||
<category name="Text" colour="#ffd866">
|
||
<block type="text"></block>
|
||
<block type="text_print"></block>
|
||
</category>
|
||
<sep gap="8"></sep>
|
||
<category name="Variablen" custom="VARIABLE" colour="#ff75b5"></category>
|
||
<category name="Funktionen" custom="PROCEDURE" colour="#e056fd"></category>
|
||
</xml>
|
||
|
||
<section id="console" aria-label="Ausgabe">
|
||
<div id="consoleHead">
|
||
<span id="statusDot" title="Status"></span>
|
||
<strong>Konsole</strong>
|
||
<span id="statusText">bereit</span>
|
||
<div class="spacer"></div>
|
||
<button class="btn ghost" id="btnClear">Leeren</button>
|
||
<button class="btn ghost" id="btnToggle">Ein-/Ausklappen</button>
|
||
</div>
|
||
<pre id="output">🌲 Terminal wartet …</pre>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
// eigenes Dark-Theme
|
||
const CrumbDark = Blockly.Theme.defineTheme('CrumbDark', {
|
||
base: Blockly.Themes.Dark,
|
||
componentStyles: {
|
||
workspaceBackgroundColour: '#0f1319',
|
||
toolboxBackgroundColour: '#0e1217',
|
||
toolboxForegroundColour: '#e9edf5',
|
||
flyoutBackgroundColour: '#0f1319',
|
||
flyoutForegroundColour: '#e9edf5',
|
||
scrollbarColour: '#2a3240',
|
||
insertionMarkerColour: '#4caf50',
|
||
insertionMarkerOpacity: 0.35,
|
||
cursorColour: '#4caf50',
|
||
}
|
||
});
|
||
|
||
const workspace = Blockly.inject('blocklyDiv', {
|
||
toolbox: document.getElementById('toolbox'),
|
||
theme: CrumbDark,
|
||
renderer: 'zelos',
|
||
trashcan: true,
|
||
grid: { spacing: 28, length: 2, colour: '#1a2029', snap: true },
|
||
move: { scrollbars: true, drag: true, wheel: true },
|
||
zoom: { controls: false, wheel: true, startScale: 1.15, maxScale: 2.2, minScale: 0.7, scaleSpeed: 1.2 },
|
||
});
|
||
window.addEventListener('resize', () => Blockly.svgResize(workspace));
|
||
|
||
// VORDEFINIERTE VARIABLEN für die Kids
|
||
const PRESET_VARS = [
|
||
'energie_wh','verbrauch_wh','sensor_temp_c','power_w','wasser_ml','zeit_s'
|
||
];
|
||
for (const name of PRESET_VARS) {
|
||
try { workspace.createVariable(name); } catch(_) {}
|
||
}
|
||
|
||
// UI helpers
|
||
const outputEl = document.getElementById('output');
|
||
const statusDot = document.getElementById('statusDot');
|
||
const statusText = document.getElementById('statusText');
|
||
const consoleBox = document.getElementById('console');
|
||
function setStatus(color, text){ statusDot.style.background=color; statusText.textContent=text||''; }
|
||
function log(line){ outputEl.textContent += (outputEl.textContent.endsWith('\n')?'':'\n')+ line; outputEl.scrollTop = outputEl.scrollHeight; }
|
||
function resetOutput(msg='🌲 Terminal wartet …'){ outputEl.textContent = msg; }
|
||
|
||
// Buttons
|
||
document.getElementById('btnZoomIn').onclick = () => workspace.zoomCenter(1);
|
||
document.getElementById('btnZoomOut').onclick = () => workspace.zoomCenter(-1);
|
||
document.getElementById('btnTrash').onclick = () => workspace.clear();
|
||
document.getElementById('btnClear').onclick = () => resetOutput('— leere Ausgabe —');
|
||
let collapsed=false;
|
||
document.getElementById('btnToggle').onclick = () => { collapsed=!collapsed; consoleBox.style.display = collapsed?'none':'grid'; };
|
||
|
||
// Demo-Workspace (Wasserkocher) – via DOMParser, ohne Blockly.Xml.textToDom
|
||
const DEMO_XML = `
|
||
<xml xmlns="https://developers.google.com/blockly/xml">
|
||
<variables>
|
||
<variable>energie_wh</variable>
|
||
<variable>verbrauch_wh</variable>
|
||
<variable>sensor_temp_c</variable>
|
||
<variable>power_w</variable>
|
||
<variable>wasser_ml</variable>
|
||
<variable>zeit_s</variable>
|
||
</variables>
|
||
<block type="variables_set" x="20" y="20">
|
||
<field name="VAR">sensor_temp_c</field>
|
||
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
|
||
<next>
|
||
<block type="variables_set">
|
||
<field name="VAR">power_w</field>
|
||
<value name="VALUE"><block type="math_number"><field name="NUM">800</field></block></value>
|
||
<next>
|
||
<block type="controls_whileUntil">
|
||
<field name="MODE">WHILE</field>
|
||
<value name="BOOL">
|
||
<block type="logic_compare">
|
||
<field name="OP">LT</field>
|
||
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
|
||
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
|
||
</block>
|
||
</value>
|
||
<statement name="DO">
|
||
<block type="variables_set">
|
||
<field name="VAR">sensor_temp_c</field>
|
||
<value name="VALUE">
|
||
<block type="math_arithmetic">
|
||
<field name="OP">ADD</field>
|
||
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
|
||
<value name="B"><block type="math_number"><field name="NUM">10</field></block></value>
|
||
</block>
|
||
</value>
|
||
<next>
|
||
<block type="text_print">
|
||
<value name="TEXT">
|
||
<block type="text"><field name="TEXT">Erhitze… Temp=</field></block>
|
||
</value>
|
||
<next>
|
||
<block type="text_print">
|
||
<value name="TEXT"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
|
||
</block>
|
||
</next>
|
||
</block>
|
||
</next>
|
||
</block>
|
||
</statement>
|
||
<next>
|
||
<block type="text_print">
|
||
<value name="TEXT"><block type="text"><field name="TEXT">Wasser kocht! 🔥</field></block></value>
|
||
</block>
|
||
</next>
|
||
</block>
|
||
</next>
|
||
</block>
|
||
</next>
|
||
</block>
|
||
</xml>`.trim();
|
||
|
||
function loadDemo(){
|
||
try{
|
||
workspace.clear();
|
||
const dom = new DOMParser().parseFromString(DEMO_XML, 'text/xml').documentElement;
|
||
// domToWorkspace ist in allen Builds vorhanden
|
||
Blockly.Xml.domToWorkspace(dom, workspace);
|
||
Blockly.svgResize(workspace);
|
||
setStatus('#4caf50','Demo geladen');
|
||
resetOutput('🧪 Demo geladen – „Wasserkocher“ bereit.');
|
||
}catch(e){
|
||
setStatus('#e74c3c','Demo-Fehler');
|
||
resetOutput('❌ Demo konnte nicht geladen werden:\n' + (e?.message||e));
|
||
console.error(e);
|
||
}
|
||
}
|
||
document.getElementById('btnDemo').onclick = loadDemo;
|
||
|
||
// Run nur aktiv wenn es Blöcke gibt
|
||
const btnRun = document.getElementById('btnRun');
|
||
function updateRunDisabled(){
|
||
const disabled = workspace.getAllBlocks(false).length===0;
|
||
btnRun.disabled = disabled;
|
||
btnRun.style.opacity = disabled ? .55 : 1;
|
||
btnRun.style.cursor = disabled ? 'not-allowed' : 'pointer';
|
||
}
|
||
workspace.addChangeListener(updateRunDisabled);
|
||
updateRunDisabled();
|
||
|
||
// Ausführen → Bridge (mit Echo-Fallback)
|
||
async function runCode(){
|
||
const code = Blockly.Python.workspaceToCode(workspace);
|
||
resetOutput('📤 Sende an Terminal…\n\n' + code);
|
||
setStatus('#f1c40f','sende…');
|
||
|
||
const ctrl = new AbortController();
|
||
const timer = setTimeout(()=>ctrl.abort(), 10000);
|
||
|
||
async function post(url){
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ blockcode: code }),
|
||
signal: ctrl.signal
|
||
});
|
||
clearTimeout(timer);
|
||
if(!res.ok) throw new Error('HTTP '+res.status+' '+(await res.text()).slice(0,400));
|
||
return res.json().catch(()=>({}));
|
||
}
|
||
|
||
try{
|
||
let data;
|
||
try{
|
||
// deine echte Bridge
|
||
data = await post('/crumbapi/blockly-terminal');
|
||
}catch(_){
|
||
// Fallback-Echo (damit Kids sofort Feedback sehen)
|
||
data = { ok:true, mode:'echo-fallback', code:code };
|
||
}
|
||
setStatus('#4caf50','fertig');
|
||
log('\n✅ Antwort:\n' + JSON.stringify(data, null, 2));
|
||
}catch(err){
|
||
setStatus('#e74c3c','Fehler');
|
||
log('\n❌ Fehler beim Senden:\n' + (err?.message||err));
|
||
}
|
||
}
|
||
btnRun.addEventListener('click', runCode);
|
||
|
||
// Startzustand: Demo direkt laden
|
||
loadDemo();
|
||
</script>
|
||
</body>
|
||
</html>
|